revanced-patcher/docs/2_2_1_fingerprinting.md
2024-02-25 03:30:08 +01:00

9.9 KiB
Raw Blame History


                       

Continuing the legacy of Vanced

🔎 Fingerprinting

In the context of ReVanced, fingerprinting is primarily used to resolve methods with a limited amount of known information. Methods with obfuscated names that change with each update are primary candidates for fingerprinting. The goal of fingerprinting is to uniquely identify a method by capturing various attributes, such as the return type, access flags, an opcode pattern, strings, and more.

Example fingerprint

Throughout the documentation, the following example will be used to demonstrate the concepts of fingerprints:


package app.revanced.patches.ads.fingerprints

object ShowAdsFingerprint : MethodFingerprint(
    returnType = "Z",
    accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
    parameters = listOf("Z"),
    opcodes = listOf(Opcode.RETURN),
    strings = listOf("pro"),
    customFingerprint = { (methodDef, classDef) -> methodDef.definingClass == "Lcom/some/app/ads/AdsLoader;" }
)

🔎 Reconstructing the original code from a fingerprint

The following code is reconstructed from the fingerprint to understand how a fingerprint is created.

The fingerprint contains the following information:

  • Method signature:

    returnType = "Z",
    access = AccessFlags.PUBLIC or AccessFlags.FINAL,
    parameters = listOf("Z"),
    
  • Method implementation:

    opcodes = listOf(Opcode.RETURN)
    strings = listOf("pro"),
    
  • Package and class name:

    customFingerprint = { (methodDef, classDef) -> methodDef.definingClass == "Lcom/some/app/ads/AdsLoader;"}
    

With this information, the original code can be reconstructed:

    package com.some.app.ads;

    <accessFlags> class AdsLoader {
        public final boolean <methodName>(boolean <parameter>) {
            // ...

            var userStatus = "pro";

            // ...

            return <returnValue>;
        }
    }

Tip

A fingerprint should contain information about a method likely to remain the same across updates. A method's name is not included in the fingerprint because it is likely to change with each update in an obfuscated app. In contrast, the return type, access flags, parameters, patterns of opcodes, and strings are likely to remain the same.

🔨 How to use fingerprints

After creating a fingerprint, add it to the constructor of a BytecodePatch:

object DisableAdsPatch : BytecodePatch(
    setOf(ShowAdsFingerprint)
) {
    // ...
 }

Note

Fingerprints passed to the constructor of BytecodePatch are resolved by ReVanced Patcher before the patch is executed.

Tip

Multiple patches can share fingerprints. If a fingerprint is resolved once, it will not be resolved again.

Tip

If a fingerprint has an opcode pattern, you can use the FuzzyPatternScanMethod annotation to fuzzy match the pattern. Opcode pattern arrays can contain null values to indicate that the opcode at the index is unknown. Any opcode will match to a null value.

Warning

If the fingerprint can not be resolved because it does not match any method, the result of a fingerprint is null.

Once the fingerprint is resolved, the result can be used in the patch:

object DisableAdsPatch : BytecodePatch(
  setOf(ShowAdsFingerprint)
) {
    override fun execute(context: BytecodeContext) {
        val result = ShowAdsFingerprint.result
            ?: throw PatchException("ShowAdsFingerprint not found")

        // ...
    }
}

The result of a fingerprint that resolved successfully contains mutable and immutable references to the method and the class it is defined in.

class MethodFingerprintResult(
    val method: Method,
    val classDef: ClassDef,
    val scanResult: MethodFingerprintScanResult,
    // ...
) {
    val mutableClass by lazy { /* ... */ }
    val mutableMethod by lazy { /* ... */ }

    // ...
}

class MethodFingerprintScanResult(
    val patternScanResult: PatternScanResult?,
    val stringsScanResult: StringsScanResult?,
) {
    class StringsScanResult(val matches: List<StringMatch>) {
        class StringMatch(val string: String, val index: Int)
    }

    class PatternScanResult(
        val startIndex: Int,
        val endIndex: Int,
        // ...
    ) {
        // ...
    }
}

🏹 Manual resolution of fingerprints

Unless a fingerprint is added to the constructor of BytecodePatch, the fingerprint will not be resolved automatically by ReVanced Patcher before the patch is executed. Instead, the fingerprint can be resolved manually using various overloads of the resolve function of a fingerprint.

You can resolve a fingerprint in the following ways:

  • On a list of classes, if the fingerprint can resolve on a known subset of classes

    If you have a known list of classes you know the fingerprint can resolve on, you can resolve the fingerprint on the list of classes:

    override fun execute(context: BytecodeContext) {
        val result = ShowAdsFingerprint.also { it.resolve(context, context.classes) }.result
            ?: throw PatchException("ShowAdsFingerprint not found")
    
            // ...
    }
    
  • On a single class, if the fingerprint can resolve on a single known class

    If you know the fingerprint can resolve to a method in a specific class, you can resolve the fingerprint on the class:

    override fun execute(context: BytecodeContext) {
        val adsLoaderClass = context.classes.single { it.name == "Lcom/some/app/ads/Loader;" }
    
        val result = ShowAdsFingerprint.also { it.resolve(context, adsLoaderClass) }.result
            ?: throw PatchException("ShowAdsFingerprint not found")
    
        // ...
    }
    
  • On a single method, to extract certain information about a method

    The result of a fingerprint contains useful information about the method, such as the start and end index of an opcode pattern or the indices of the instructions with certain string references. A fingerprint can be leveraged to extract such information from a method instead of manually figuring it out:

    override fun execute(context: BytecodeContext) {
        val adsFingerprintResult = ShowAdsFingerprint.result
            ?: throw PatchException("ShowAdsFingerprint not found")
    
        val proStringsFingerprint = object : MethodFingerprint(
            strings = listOf("free", "trial")
        ) {}
    
        proStringsFingerprint.also {
            it.resolve(context, adsFingerprintResult.method)
        }.result?.let { result ->
            result.scanResult.stringsScanResult!!.matches.forEach { match ->
                println("The index of the string '${match.string}' is ${match.index}")
            }
    
        } ?: throw PatchException("pro strings fingerprint not found")
    }
    

Tip

To see real-world examples of fingerprints, check out the ReVanced Patches repository.

⏭️ What's next

The next page discusses the structure and conventions of patches.

Continue: 📜 Project structure and conventions