revanced-patcher/docs/2_2_1_fingerprinting.md
oSumAtrIX 11a911dc67 feat: Convert APIs to Kotlin DSL (#298)
This commit converts various APIs to Kotlin DSL.

BREAKING CHANGE: Various old APIs are removed, and DSL APIs are added instead.
2024-08-06 16:53:42 +02:00

9.3 KiB
Raw Blame History


                       

Continuing the legacy of Vanced

🔎 Fingerprinting

In the context of ReVanced, fingerprinting is primarily used to match 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

fingerprint {
    accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
    returns("Z")
    parameters("Z")
    opcodes(Opcode.RETURN)
    strings("pro")
    custom { (method, classDef) -> method.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:

    accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
    returns("Z")
    parameters("Z")
    
  • Method implementation:

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

    custom = { (method, classDef) -> method.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 will likely 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

Fingerprints can be added to a patch by directly creating and adding them or by invoking them manually. Fingerprints added to a patch are matched by ReVanced Patcher before the patch is executed.

val fingerprint = fingerprint {
    // ...
}

val patch = bytecodePatch {
    // Directly create and add a fingerprint.
    fingerprint {
        // ...
    }

    // Add a fingerprint manually by invoking it.
    fingerprint()
}

Tip

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

Tip

If a fingerprint has an opcode pattern, you can use the fuzzyPatternScanThreshhold parameter of the opcode function to fuzzy match the pattern.
null can be used as a wildcard to match any opcode:

fingerprint(fuzzyPatternScanThreshhold = 2) {
   opcodes(
       Opcode.ICONST_0,
       null,
       Opcode.ICONST_1,
       Opcode.IRETURN,
   )
}

Once the fingerprint is matched, the match can be used in the patch:

val patch = bytecodePatch {
    // Add a fingerprint and delegate its match to a variable.
    val match by showAdsFingerprint()
    val match2 by fingerprint {
        // ...
    }
  
    execute {
        val method = match.method
        val method2 = match2.method
    }
}

Warning

If the fingerprint can not be matched to any method, the match of a fingerprint is null. If such a match is delegated to a variable, accessing it will raise an exception.

The match of a fingerprint contains mutable and immutable references to the method and the class it matches to.

class Match(
    val method: Method,
    val classDef: ClassDef,
    val patternMatch: Match.PatternMatch?,
    val stringMatches: List<Match.StringMatch>?,
    // ...
) {
    val mutableClass by lazy { /* ... */ }
    val mutableMethod by lazy { /* ... */ }

    // ...
}

🏹 Manual matching of fingerprints

Unless a fingerprint is added to a patch, the fingerprint will not be matched automatically by ReVanced Patcher before the patch is executed. Instead, the fingerprint can be matched manually using various overloads of a fingerprint's match function.

You can match a fingerprint the following ways:

  • In a list of classes, if the fingerprint can match in a known subset of classes

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

      execute { context ->
          val match = showAdsFingerprint.apply { 
              match(context, context.classes) 
          }.match ?: throw PatchException("No match found")
      }
    
  • In a single class, if the fingerprint can match in a single known class

    If you know the fingerprint can match a method in a specific class, you can match the fingerprint in the class:

    execute { context ->
        val adsLoaderClass = context.classes.single { it.name == "Lcom/some/app/ads/Loader;" }
    
        val match = showAdsFingerprint.apply { 
          match(context, adsLoaderClass)
        }.match ?: throw PatchException("No match found")
    }
    
  • Match a single method, to extract certain information about it

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

    execute { context ->
        val proStringsFingerprint = fingerprint {
            strings("free", "trial")
        }
    
        proStringsFingerprint.apply {
            match(context, adsFingerprintMatch.method)
        }.match?.let { match ->
            match.stringMatches.forEach { match ->
                println("The index of the string '${match.string}' is ${match.index}")
            }
        } ?: throw PatchException("No match found")
    }
    

Tip

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

⏭️ What's next

The next page discusses the structure and conventions of patches.

Continue: 📜 Project structure and conventions