Skip to content

Commit

Permalink
feat: Improve Fingerprint API
Browse files Browse the repository at this point in the history
Fingerprints can now be matched easily without having to add them to a patch first.

BREAKING CHANGE: Many APIs have been changed.
  • Loading branch information
oSumAtrIX committed Oct 27, 2024
1 parent 7be0cd8 commit 92eaba8
Show file tree
Hide file tree
Showing 12 changed files with 315 additions and 334 deletions.
49 changes: 20 additions & 29 deletions api/revanced-patcher.api
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
public final class app/revanced/patcher/Fingerprint {
public final fun getMatch ()Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Z
}

public final class app/revanced/patcher/FingerprintBuilder {
Expand All @@ -18,20 +15,17 @@ public final class app/revanced/patcher/FingerprintBuilder {

public final class app/revanced/patcher/FingerprintKt {
public static final fun fingerprint (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/Fingerprint;
public static final fun fingerprint (Lapp/revanced/patcher/patch/BytecodePatchBuilder;ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint;
public static synthetic fun fingerprint$default (ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/Fingerprint;
public static synthetic fun fingerprint$default (Lapp/revanced/patcher/patch/BytecodePatchBuilder;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint;
}

public abstract interface annotation class app/revanced/patcher/InternalApi : java/lang/annotation/Annotation {
}

public final class app/revanced/patcher/Match {
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lapp/revanced/patcher/Match$PatternMatch;Ljava/util/List;Lapp/revanced/patcher/patch/BytecodePatchContext;)V
public final fun getClassDef ()Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public final fun getMethod ()Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun getMutableClass ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public final fun getMutableMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun getClassDef ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public final fun getMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun getOriginalClassDef ()Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public final fun getOriginalMethod ()Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun getPatternMatch ()Lapp/revanced/patcher/Match$PatternMatch;
public final fun getStringMatches ()Ljava/util/List;
}
Expand Down Expand Up @@ -63,8 +57,8 @@ public final class app/revanced/patcher/Patcher : java/io/Closeable {
}

public final class app/revanced/patcher/PatcherConfig {
public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Z)V
public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
}

public final class app/revanced/patcher/PatcherContext : java/io/Closeable {
Expand Down Expand Up @@ -135,30 +129,27 @@ public final class app/revanced/patcher/extensions/InstructionExtensions {
}

public final class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch {
public final fun getExtension ()Ljava/io/InputStream;
public final fun getFingerprints ()Ljava/util/Set;
public final fun getExtensionInputStream ()Ljava/util/function/Supplier;
public fun toString ()Ljava/lang/String;
}

public final class app/revanced/patcher/patch/BytecodePatchBuilder : app/revanced/patcher/patch/PatchBuilder {
public synthetic fun build$revanced_patcher ()Lapp/revanced/patcher/patch/Patch;
public final fun extendWith (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatchBuilder;
public final fun getExtension ()Ljava/io/InputStream;
public final fun invoke (Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint;
public final fun setExtension (Ljava/io/InputStream;)V
}

public final class app/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint {
public final fun getValue (Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/Match;
public final fun getExtensionInputStream ()Ljava/util/function/Supplier;
public final fun setExtensionInputStream (Ljava/util/function/Supplier;)V
}

public final class app/revanced/patcher/patch/BytecodePatchContext : app/revanced/patcher/patch/PatchContext, java/io/Closeable {
public final fun classBy (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/ClassProxy;
public final fun classByType (Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/ClassProxy;
public fun close ()V
public synthetic fun get ()Ljava/lang/Object;
public fun get ()Ljava/util/Set;
public final fun getClasses ()Lapp/revanced/patcher/util/ProxyClassList;
public final fun getMatch (Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patcher/Match;
public final fun getValue (Lapp/revanced/patcher/Fingerprint;Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/Fingerprint;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/Fingerprint;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match;
public final fun navigate (Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/util/MethodNavigator;
public final fun proxy (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/util/proxy/ClassProxy;
}
Expand Down Expand Up @@ -286,7 +277,7 @@ public final class app/revanced/patcher/patch/Options : java/util/Map, kotlin/jv
}

public abstract class app/revanced/patcher/patch/Patch {
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;Ljava/util/Set;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun execute (Lapp/revanced/patcher/patch/PatchContext;)V
public final fun finalize (Lapp/revanced/patcher/patch/PatchContext;)V
public final fun getCompatiblePackages ()Ljava/util/Set;
Expand All @@ -303,22 +294,22 @@ public abstract class app/revanced/patcher/patch/PatchBuilder {
public final fun compatibleWith ([Ljava/lang/String;)V
public final fun compatibleWith ([Lkotlin/Pair;)V
public final fun dependsOn ([Lapp/revanced/patcher/patch/Patch;)V
public final fun execute (Lkotlin/jvm/functions/Function2;)V
public final fun finalize (Lkotlin/jvm/functions/Function2;)V
public final fun execute (Lkotlin/jvm/functions/Function1;)V
public final fun finalize (Lkotlin/jvm/functions/Function1;)V
protected final fun getCompatiblePackages ()Ljava/util/Set;
protected final fun getDependencies ()Ljava/util/Set;
protected final fun getDescription ()Ljava/lang/String;
protected final fun getExecutionBlock ()Lkotlin/jvm/functions/Function2;
protected final fun getFinalizeBlock ()Lkotlin/jvm/functions/Function2;
protected final fun getExecutionBlock ()Lkotlin/jvm/functions/Function1;
protected final fun getFinalizeBlock ()Lkotlin/jvm/functions/Function1;
protected final fun getName ()Ljava/lang/String;
protected final fun getOptions ()Ljava/util/Set;
protected final fun getUse ()Z
public final fun invoke (Lapp/revanced/patcher/patch/Option;)Lapp/revanced/patcher/patch/Option;
public final fun invoke (Ljava/lang/String;[Ljava/lang/String;)Lkotlin/Pair;
protected final fun setCompatiblePackages (Ljava/util/Set;)V
protected final fun setDependencies (Ljava/util/Set;)V
protected final fun setExecutionBlock (Lkotlin/jvm/functions/Function2;)V
protected final fun setFinalizeBlock (Lkotlin/jvm/functions/Function2;)V
protected final fun setExecutionBlock (Lkotlin/jvm/functions/Function1;)V
protected final fun setFinalizeBlock (Lkotlin/jvm/functions/Function1;)V
}

public abstract interface class app/revanced/patcher/patch/PatchContext : java/util/function/Supplier {
Expand Down
4 changes: 4 additions & 0 deletions docs/2_1_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ To start developing patches with ReVanced Patcher, you must prepare a developmen

Throughout the documentation, [ReVanced Patches](https://github.com/revanced/revanced-patches) will be used as an example project.

> [!NOTE]
> To start a fresh project,
> you can use the [ReVanced Patches template](https://github.com/revanced/revanced-patches-template).
1. Clone the repository

```bash
Expand Down
138 changes: 76 additions & 62 deletions docs/2_2_1_fingerprinting.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,16 @@

# 🔎 Fingerprinting

In the context of ReVanced, fingerprinting is primarily used to match methods with a limited amount of known information.
In the context of ReVanced, a fingerprint is a partial description of a method.
It is used to uniquely match a method by its characteristics.
Fingerprinting is 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:
An example fingerprint is shown below:

```kt

Expand All @@ -79,11 +81,11 @@ fingerprint {
parameters("Z")
opcodes(Opcode.RETURN)
strings("pro")
custom { (method, classDef) -> method.definingClass == "Lcom/some/app/ads/AdsLoader;" }
custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" }
}
```

## 🔎 Reconstructing the original code from a fingerprint
## 🔎 Reconstructing the original code from the example fingerprint from above

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

Expand All @@ -107,7 +109,7 @@ The fingerprint contains the following information:
- Package and class name:

```kt
custom = { (method, classDef) -> method.definingClass == "Lcom/some/app/ads/AdsLoader;"}
custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" }
```

With this information, the original code can be reconstructed:
Expand All @@ -128,34 +130,71 @@ With this information, the original code can be reconstructed:
}
```

Using that fingerprint, this method can be matched uniquely from all other methods.

> [!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.
A fingerprint is matched to a method,
once the `match` property of the fingerprint is accessed in a patch's `execute` scope:

```kt
val fingerprint = fingerprint {
// ...
}

val patch = bytecodePatch {
// Directly create and add a fingerprint.
fingerprint {
// ...
execute {
val match = fingerprint.match!!
}
}
```

The fingerprint won't be matched again, if it has already been matched once.
This makes it useful, to share fingerprints between multiple patches, and let the first patch match the fingerprint:

```
// Either of these two patches will match the fingerprint first and the other patch can reuse the match:
val mainActivityPatch1 = bytecodePatch {
execute {
val match = mainActivityOnCreateFingerprint.match!!
}
}
// Add a fingerprint manually by invoking it.
fingerprint()
val mainActivityPatch2 = bytecodePatch {
execute {
val match = mainActivityOnCreateFingerprint.match!!
}
}
```

> [!TIP]
> Multiple patches can share fingerprints. If a fingerprint is matched once, it will not be matched again.
A fingerprint match can also be delegated to a variable for convenience without the need to check for `null`:
```kt
val fingerprint = fingerprint {
// ...
}

val patch = bytecodePatch {
execute {
// Alternative to fingerprint.match ?: throw PatchException("No match found")
val match by fingerprint.match

try {
match.method
} catch (e: PatchException) {
// Handle the exception for example.
}
}
}
```

> [!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.
> [!TIP]
> If a fingerprint has an opcode pattern, you can use the `fuzzyPatternScanThreshhold` parameter of the `opcode`
Expand All @@ -172,63 +211,42 @@ val patch = bytecodePatch {
> )
>}
> ```
Once the fingerprint is matched, the match can be used in the patch:
```kt
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.
>
The match of a fingerprint contains references to the original method and class definition of the method:
```kt
class Match(
val method: Method,
val classDef: ClassDef,
val originalMethod: Method,
val originalClassDef: ClassDef,
val patternMatch: Match.PatternMatch?,
val stringMatches: List<Match.StringMatch>?,
// ...
) {
val mutableClass by lazy { /* ... */ }
val mutableMethod by lazy { /* ... */ }
val classDef by lazy { /* ... */ }
val method by lazy { /* ... */ }
// ...
}
```
## 🏹 Manual matching of fingerprints
The `classDef` and `method` properties can be used to make changes to the class or method.
They are lazy properties, so they are only computed
and will effectively replace the original method or class definition when accessed.

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.
## 🏹 Manually matching fingerprints

You can match a fingerprint the following ways:
By default, a fingerprint is matched automatically against all classes when the `match` property is accessed.

Instead, the fingerprint can be matched manually using various overloads of a fingerprint's `match` function:

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

```kt
execute { context ->
val match = showAdsFingerprint.apply {
match(context, context.classes)
}.match ?: throw PatchException("No match found")
execute {
val match = showAdsFingerprint.match(classes) ?: throw PatchException("No match found")
}
```

Expand All @@ -237,30 +255,26 @@ you can match the fingerprint on the list of classes:
If you know the fingerprint can match a method in a specific class, you can match the fingerprint in the class:

```kt
execute { context ->
val adsLoaderClass = context.classes.single { it.name == "Lcom/some/app/ads/Loader;" }
execute {
val adsLoaderClass = classes.single { it.name == "Lcom/some/app/ads/Loader;" }

val match = showAdsFingerprint.apply {
match(context, adsLoaderClass)
}.match ?: throw PatchException("No match found")
val match = showAdsFingerprint.match(context, adsLoaderClass) ?: 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.
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:

```kt
execute { context ->
val proStringsFingerprint = fingerprint {
execute {
val currentPlanFingerprint = fingerprint {
strings("free", "trial")
}

proStringsFingerprint.apply {
match(context, adsFingerprintMatch.method)
}.match?.let { match ->
currentPlanFingerprint.match(adsFingerprintMatch.method)?.let { match ->
match.stringMatches.forEach { match ->
println("The index of the string '${match.string}' is ${match.index}")
}
Expand Down
Loading

0 comments on commit 92eaba8

Please sign in to comment.