--- name: fingerprinting-guide description: Complete guide to Morphe fingerprinting — how to identify obfuscated methods using stable characteristics. Use when creating fingerprints, debugging fingerprint match failures, or understanding how filters work. --- > **When to use:** User needs to create or debug fingerprints. NEVER use obfuscated names. Always verify against smali, not jadx. Use `filters` (ordered) over `strings` (unordered). Declare as `object` classes for named stack traces. # Morphe Fingerprinting — Official Reference > ⚠️ Pinned source: morphe-patcher `6f189f9`. Official docs may lag the source — the pinned > source wins if anything contradicts this file. A fingerprint is a partial description of a method used to uniquely match it by stable characteristics that survive app updates. Obfuscated names change every release — fingerprints match on return type, access flags, parameters, and instruction patterns instead. ## Fingerprint Declaration — Preferred Style Declare as **`object` classes** — this gives named stack traces when a fingerprint fails to match: ```kotlin object MyFingerprint : Fingerprint( definingClass = "Lcom/example/Class;", // StringComparisonType semantics name = "methodName", // Only for non-obfuscated methods accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL), returnType = "Z", parameters = listOf("Ljava/lang/String;", "I", "L"), // "L" for obfuscated classes // Ordered instruction filters (must appear in same order as target method) filters = listOf( fieldAccess(opcode = Opcode.IGET, definingClass = "this", type = "Ljava/util/Map;"), string("showBannerAds"), methodCall(definingClass = "Ljava/lang/String;", name = "equals"), opcode(Opcode.MOVE_RESULT, InstructionLocation.MatchAfterImmediately()), literal(1337), opcode(Opcode.IF_EQ), ), // Unordered string matching (for methods with many strings in random order) strings = listOf("unordered1", "unordered2"), // Custom predicate custom = { method, classDef -> classDef.type == "Lcom/target/Class;" }, // Find class via another fingerprint first classFingerprint = AnotherFingerprint, ) ``` The old DSL builder (`fingerprint { … }`) is deprecated. Always use `object X : Fingerprint(…)`. ## Access Flags — Exact Bitmask `accessFlags` matches the **exact** bitmask. Every flag present in the method must be listed: ```kotlin // ✅ Public static — list both accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.STATIC) // ✅ Public static final — list all three accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL) // ❌ Wrong — won't match a "public static final" method if FINAL is omitted accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.STATIC) ``` Always verify the exact flags from smali. The common ones: | AccessFlags value | Smali keyword | |-------------------|---------------| | `PUBLIC` | `public` | | `PRIVATE` | `private` | | `PROTECTED` | `protected` | | `STATIC` | `static` | | `FINAL` | `final` | | `ABSTRACT` | `abstract` | | `CONSTRUCTOR` | constructor (``) | | `SYNTHETIC` | `synthetic` | ## Filter Types | Filter | Usage | |--------|-------| | `string("text")` | Match const-string instruction (EQUALS by default) | | `string("text", StringComparisonType.CONTAINS)` | Match string with custom comparison | | `methodCall(definingClass, name, parameters, returnType)` | Match invoke-* instruction | | `methodCall(smali = "Landroid/net/Uri;->parse(...)Landroid/net/Uri;")` | Smali shorthand | | `fieldAccess(opcode, definingClass, name, type)` | Match field get/put | | `fieldAccess(smali = "Landroid/os/Build;->MODEL:Ljava/lang/String;")` | Smali shorthand | | `opcode(Opcode.X)` | Match specific opcode | | `literal(value)` | Match const literal (Long, Int, Double, Float, or `() -> Long?` lambda) | | `resourceLiteral(ResourceType.ID, "view_name")` | Match resource ID literal | | `newInstance("Lcom/example/Foo;")` | Match `new-instance` / `new-array` | | `instanceOf("Lcom/example/Foo;")` | Match `instance-of` | | `checkCast("Lcom/example/Foo;")` | Match `check-cast` | | `anyInstruction(f1, f2)` | Match any alternative (for version differences) | For `methodCall`, `fieldAccess`, `newInstance`, `instanceOf`, and `checkCast`, the comparison type is inferred from the type declaration: exact types (e.g. `Lcom/Foo;`) use EQUALS, prefixes (e.g. `Lcom/`) use STARTS_WITH, and suffixes (e.g. `/Foo;`) use ENDS_WITH. ## InstructionLocation Options | Location | Description | |----------|-------------| | `MatchAfterAnywhere()` | Default — match anywhere after previous filter | | `MatchAfterImmediately()` | Must be immediately after previous filter (MOVE_RESULT etc.) | | `MatchAfterWithin(n)` | Within n unmatched instructions of previous filter | | `MatchFirst()` | First instruction of method; only valid for first filter | | ~~`MatchAfterAtLeast`~~ | **Deprecated** — avoid; will be removed | | ~~`MatchAfterRange`~~ | **Deprecated** — avoid; will be removed | ## String Declarations — Two Ways 1. **Preferred — ordered via filters**: `filters = listOf(string("foo"), string("bar"))` — order must match target method 2. **Unordered via strings**: `strings = listOf("foo", "bar")` — matches in any order, useful for enums with many strings ## Using Fingerprints in Patches ```kotlin execute { // Auto-matches on first access, cached for reuse MyFingerprint.method.addInstructions(0, "...") // Access instruction match indices (only when filters are declared) val index = MyFingerprint.instructionMatches[0].index val reg = MyFingerprint.instructionMatches[0].getInstruction().registerA // Null-safe instructionMatches — doesn't throw if no filters val matchesOrNull = MyFingerprint.instructionMatchesOrNull // Navigate to called method from a methodCall filter match val calledMethod = MyFingerprint.instructionMatches[0].getMethodCalled() // Navigate to accessed field from a fieldAccess filter match val field = MyFingerprint.instructionMatches[1].getFieldAccessed() // Access class val classDef = MyFingerprint.originalClassDef // Null-safe access val methodOrNull = MyFingerprint.methodOrNull // Match all occurrences — with optional count range Fingerprint(filters = listOf(string("target"))).matchAll() .forEach { match -> match.method.apply { /* modify */ } } // Match all, require exactly 2–4 matches MyFingerprint.matchAll(2..4).forEach { /* … */ } // Manual matching in specific class MyFingerprint.match(SomeOtherFingerprint.originalClassDef) } ``` ## Fingerprint Properties | Property | Returns | On no match | |----------|---------|-------------| | `originalClassDef` | Immutable class | Exception | | `originalClassDefOrNull` | Immutable class | null | | `originalMethod` | Immutable method | null | | `classDef` | Mutable class (replaces original) | Exception | | `method` | Mutable method (replaces original) | Exception | | `methodOrNull` | Mutable method | null | | `instructionMatches` | `List` | Exception if no filters | | `instructionMatchesOrNull` | `List?` | null | Use `original*` for read-only access (avoids creating mutable copy). ## Class-Based Fingerprint Chaining Find class via one fingerprint, then find method within it: ```kotlin object AdClassFingerprint : Fingerprint( name = "toString", strings = listOf("classField="), ) object ShowAdFingerprint : Fingerprint( classFingerprint = AdClassFingerprint, returnType = "Z", filters = listOf( methodCall(name = "getValue", returnType = "Z"), opcode(Opcode.MOVE_RESULT, InstructionLocation.MatchAfterImmediately()), ) ) ``` ## Dynamic Fingerprints (using prior match results) ```kotlin execute { val dynamicFingerprint = Fingerprint( definingClass = SomeFingerprint.originalClassDef.type, returnType = "V", filters = listOf(fieldAccess(opcode = Opcode.IPUT_BOOLEAN, reference = someField)) ) dynamicFingerprint.method.apply { /* modify */ } } ``` ## Multiple Modifications — Index Safety When modifying multiple instructions, work from last index to first: ```kotlin AdLoaderFingerprint.let { // Last filter first val filter6 = it.instructionMatches[5] it.method.removeInstruction(filter6.index) // Then earlier filter val filter4 = it.instructionMatches[3] val reg = filter4.getInstruction().registerA it.method.addInstructions(filter4.index + 1, "const/4 v$reg, 0x0") } ``` Or use `clearMatch()` + `match()` to refresh indices after modifications. ## Critical Rules - NEVER use obfuscated class/method names (`a`, `b`, `H`) - Non-obfuscated names (`isPremium`, `getEntitlements`) are safe - Filters must appear in same order as target method instructions - Declare as `object` classes for named stack traces on match failure — **preferred** - Always verify against smali bytecode, not jadx Java output - Fingerprints match once and cache — safe to share between patches - Use `"L"` for obfuscated parameter types - Use `definingClass = "this"` for self-referencing fields - `accessFlags` is an **exact bitmask** — list every flag present in smali - Do not use `MatchAfterAtLeast` or `MatchAfterRange` — both deprecated - `parametersStartsWith` is renamed to `parametersMatch` — use the new name