mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2026-01-11 13:56:16 +00:00
feat: Improve Fingerprint API (#316)
Fingerprints can now be matched easily without adding them to a patch first. BREAKING CHANGE: Many APIs have been changed.
This commit is contained in:
@@ -89,9 +89,9 @@ val patcherResult = Patcher(PatcherConfig(apkFile = File("some.apk"))).use { pat
|
||||
runBlocking {
|
||||
patcher().collect { patchResult ->
|
||||
if (patchResult.exception != null)
|
||||
logger.info("\"${patchResult.patch}\" failed:\n${patchResult.exception}")
|
||||
logger.info { "\"${patchResult.patch}\" failed:\n${patchResult.exception}" }
|
||||
else
|
||||
logger.info("\"${patchResult.patch}\" succeeded")
|
||||
logger.info { "\"${patchResult.patch}\" succeeded" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -107,27 +109,29 @@ 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:
|
||||
|
||||
```java
|
||||
package com.some.app.ads;
|
||||
package com.some.app.ads;
|
||||
|
||||
<accessFlags> class AdsLoader {
|
||||
public final boolean <methodName>(boolean <parameter>) {
|
||||
// ...
|
||||
<accessFlags> class AdsLoader {
|
||||
public final boolean <methodName>(boolean <parameter>) {
|
||||
// ...
|
||||
|
||||
var userStatus = "pro";
|
||||
var userStatus = "pro";
|
||||
|
||||
// ...
|
||||
// ...
|
||||
|
||||
return <returnValue>;
|
||||
}
|
||||
return <returnValue>;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -135,8 +139,8 @@ With this information, the original code can be reconstructed:
|
||||
|
||||
## 🔨 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 {
|
||||
@@ -144,48 +148,46 @@ val fingerprint = fingerprint {
|
||||
}
|
||||
|
||||
val patch = bytecodePatch {
|
||||
// Directly create and add a fingerprint.
|
||||
fingerprint {
|
||||
// ...
|
||||
execute {
|
||||
val match = fingerprint.match!!
|
||||
}
|
||||
|
||||
// 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:
|
||||
>
|
||||
> ```kt
|
||||
> 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:
|
||||
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:
|
||||
|
||||
```kt
|
||||
val patch = bytecodePatch {
|
||||
// Add a fingerprint and delegate its match to a variable.
|
||||
val match by showAdsFingerprint()
|
||||
val match2 by fingerprint {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Either of these two patches will match the fingerprint first and the other patch can reuse the match:
|
||||
val mainActivityPatch1 = bytecodePatch {
|
||||
execute {
|
||||
val method = match.method
|
||||
val method2 = match2.method
|
||||
val match = mainActivityOnCreateFingerprint.match!!
|
||||
}
|
||||
}
|
||||
|
||||
val mainActivityPatch2 = bytecodePatch {
|
||||
execute {
|
||||
val match = mainActivityOnCreateFingerprint.match!!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -194,30 +196,53 @@ val patch = bytecodePatch {
|
||||
> 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.
|
||||
> [!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:
|
||||
>
|
||||
> ```kt
|
||||
> fingerprint(fuzzyPatternScanThreshhold = 2) {
|
||||
> opcodes(
|
||||
> Opcode.ICONST_0,
|
||||
> null,
|
||||
> Opcode.ICONST_1,
|
||||
> Opcode.IRETURN,
|
||||
> )
|
||||
>}
|
||||
> ```
|
||||
>
|
||||
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.
|
||||
> [!TIP]
|
||||
> If only read-only access to the class or method is needed,
|
||||
> the `originalClassDef` and `originalMethod` properties can be used,
|
||||
> to avoid making a mutable copy of the class or method.
|
||||
|
||||
You can match a fingerprint the following ways:
|
||||
## 🏹 Manually matching fingerprints
|
||||
|
||||
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
|
||||
|
||||
@@ -225,11 +250,9 @@ You can match a fingerprint the following ways:
|
||||
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")
|
||||
}
|
||||
```
|
||||
|
||||
- In a **single class**, if the fingerprint can match in a single known class
|
||||
@@ -237,34 +260,39 @@ 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")
|
||||
}
|
||||
```
|
||||
|
||||
Another common usecase is to use a fingerprint to reduce the search space of a method to a single class.
|
||||
|
||||
```kt
|
||||
execute {
|
||||
// Match showAdsFingerprint in the class of the ads loader found by adsLoaderClassFingerprint.
|
||||
val match by showAdsFingerprint.match(adsLoaderClassFingerprint.match!!.classDef)
|
||||
}
|
||||
```
|
||||
|
||||
- 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 {
|
||||
strings("free", "trial")
|
||||
}
|
||||
execute {
|
||||
val currentPlanFingerprint = 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")
|
||||
currentPlanFingerprint.match(adsFingerprintMatch.method)?.let { match ->
|
||||
match.stringMatches.forEach { match ->
|
||||
println("The index of the string '${match.string}' is ${match.index}")
|
||||
}
|
||||
} ?: throw PatchException("No match found")
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -76,23 +76,23 @@ val disableAdsPatch = bytecodePatch(
|
||||
) {
|
||||
compatibleWith("com.some.app"("1.0.0"))
|
||||
|
||||
// Resource patch disables ads by patching resource files.
|
||||
// Patches can depend on other patches, executing them first.
|
||||
dependsOn(disableAdsResourcePatch)
|
||||
|
||||
// Precompiled DEX file to be merged into the patched app.
|
||||
// Merge precompiled DEX files into the patched app, before the patch is executed.
|
||||
extendWith("disable-ads.rve")
|
||||
|
||||
// Fingerprint to find the method to patch.
|
||||
val showAdsMatch by showAdsFingerprint {
|
||||
// More about fingerprints on the next page of the documentation.
|
||||
}
|
||||
|
||||
// Business logic of the patch to disable ads in the app.
|
||||
execute {
|
||||
// Fingerprint to find the method to patch.
|
||||
val showAdsMatch by showAdsFingerprint {
|
||||
// More about fingerprints on the next page of the documentation.
|
||||
}
|
||||
|
||||
// In the method that shows ads,
|
||||
// call DisableAdsPatch.shouldDisableAds() from the extension (precompiled DEX file)
|
||||
// to enable or disable ads.
|
||||
showAdsMatch.mutableMethod.addInstructions(
|
||||
showAdsMatch.method.addInstructions(
|
||||
0,
|
||||
"""
|
||||
invoke-static {}, LDisableAdsPatch;->shouldDisableAds()Z
|
||||
@@ -146,10 +146,10 @@ loadPatchesJar(patches).apply {
|
||||
The type of an option can be obtained from the `type` property of the option:
|
||||
|
||||
```kt
|
||||
option.type // The KType of the option.
|
||||
option.type // The KType of the option. Captures the full type information of the option.
|
||||
```
|
||||
|
||||
Options can be declared outside of a patch and added to a patch manually:
|
||||
Options can be declared outside a patch and added to a patch manually:
|
||||
|
||||
```kt
|
||||
val option = stringOption(key = "option")
|
||||
@@ -183,11 +183,9 @@ and use it in a patch:
|
||||
```kt
|
||||
val patch = bytecodePatch(name = "Complex patch") {
|
||||
extendWith("complex-patch.rve")
|
||||
|
||||
val match by methodFingerprint()
|
||||
|
||||
|
||||
execute {
|
||||
match.mutableMethod.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V")
|
||||
fingerprint.match!!.mutableMethod.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -96,21 +96,21 @@ Example of patches:
|
||||
@Surpress("unused")
|
||||
val bytecodePatch = bytecodePatch {
|
||||
execute {
|
||||
// TODO
|
||||
// More about this on the next page of the documentation.
|
||||
}
|
||||
}
|
||||
|
||||
@Surpress("unused")
|
||||
val rawResourcePatch = rawResourcePatch {
|
||||
execute {
|
||||
// TODO
|
||||
execute {
|
||||
// More about this on the next page of the documentation.
|
||||
}
|
||||
}
|
||||
|
||||
@Surpress("unused")
|
||||
val resourcePatch = resourcePatch {
|
||||
execute {
|
||||
// TODO
|
||||
execute {
|
||||
// More about this on the next page of the documentation.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -4,13 +4,11 @@ A handful of APIs are available to make patch development easier and more effici
|
||||
|
||||
## 📙 Overview
|
||||
|
||||
1. 👹 Mutate classes with `context.proxy(ClassDef)`
|
||||
2. 🔍 Find and proxy existing classes with `classBy(Predicate)` and `classByType(String)`
|
||||
3. 🏃 Easily access referenced methods recursively by index with `MethodNavigator`
|
||||
4. 🔨 Make use of extension functions from `BytecodeUtils` and `ResourceUtils` with certain applications
|
||||
(Available in ReVanced Patches)
|
||||
5. 💾 Read and write (decoded) resources with `ResourcePatchContext.get(Path, Boolean)`
|
||||
6. 📃 Read and write DOM files using `ResourcePatchContext.document`
|
||||
1. 👹 Create mutable replacements of classes with `proxy(ClassDef)`
|
||||
2. 🔍 Find and create mutable replaces with `classBy(Predicate)`
|
||||
3. 🏃 Navigate method calls recursively by index with `navigate(Method).at(index)`
|
||||
4. 💾 Read and write resource files with `get(Path, Boolean)`
|
||||
5. 📃 Read and write DOM files using `document`
|
||||
|
||||
### 🧰 APIs
|
||||
|
||||
|
||||
Reference in New Issue
Block a user