From 61a7533136f38d7a308e31c9f250baad00515d6a Mon Sep 17 00:00:00 2001 From: Karol Date: Thu, 24 Aug 2023 22:21:33 +0200 Subject: [PATCH] feat(Duolingo): Add `Unlock Duolingo Super` patch (#2862) --- .../IsUserSuperMethodFingerprint.kt | 14 ++++ .../UserSerializationMethodFingerprint.kt | 20 ++++++ .../patch/UnlockDuolingoSuperPatch.kt | 64 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 src/main/kotlin/app/revanced/patches/duolingo/unlocksuper/fingerprints/IsUserSuperMethodFingerprint.kt create mode 100644 src/main/kotlin/app/revanced/patches/duolingo/unlocksuper/fingerprints/UserSerializationMethodFingerprint.kt create mode 100644 src/main/kotlin/app/revanced/patches/duolingo/unlocksuper/patch/UnlockDuolingoSuperPatch.kt diff --git a/src/main/kotlin/app/revanced/patches/duolingo/unlocksuper/fingerprints/IsUserSuperMethodFingerprint.kt b/src/main/kotlin/app/revanced/patches/duolingo/unlocksuper/fingerprints/IsUserSuperMethodFingerprint.kt new file mode 100644 index 000000000..8ee88fdce --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/duolingo/unlocksuper/fingerprints/IsUserSuperMethodFingerprint.kt @@ -0,0 +1,14 @@ +package app.revanced.patches.duolingo.unlocksuper.fingerprints + +import app.revanced.patcher.extensions.or +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +object IsUserSuperMethodFingerprint : MethodFingerprint( + returnType = "Ljava/lang/Object", + parameters = listOf("Ljava/lang/Object"), + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + strings = listOf("user"), + opcodes = listOf(Opcode.IGET_BOOLEAN), +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/duolingo/unlocksuper/fingerprints/UserSerializationMethodFingerprint.kt b/src/main/kotlin/app/revanced/patches/duolingo/unlocksuper/fingerprints/UserSerializationMethodFingerprint.kt new file mode 100644 index 000000000..2d8bcdda1 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/duolingo/unlocksuper/fingerprints/UserSerializationMethodFingerprint.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.duolingo.unlocksuper.fingerprints + +import app.revanced.patcher.extensions.or +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +object UserSerializationMethodFingerprint : MethodFingerprint( + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + strings = listOf( + "betaStatus", + "coachOutfit", + "globalAmbassadorStatus", + ), + opcodes = listOf( + Opcode.MOVE_FROM16, + Opcode.IPUT_BOOLEAN, + ), +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/duolingo/unlocksuper/patch/UnlockDuolingoSuperPatch.kt b/src/main/kotlin/app/revanced/patches/duolingo/unlocksuper/patch/UnlockDuolingoSuperPatch.kt new file mode 100644 index 000000000..86d2ecbc7 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/duolingo/unlocksuper/patch/UnlockDuolingoSuperPatch.kt @@ -0,0 +1,64 @@ +package app.revanced.patches.duolingo.unlocksuper.patch + +import app.revanced.extensions.toErrorResult +import app.revanced.patcher.annotation.Compatibility +import app.revanced.patcher.annotation.Description +import app.revanced.patcher.annotation.Name +import app.revanced.patcher.annotation.Package +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.extensions.InstructionExtensions.getInstructions +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.annotations.Patch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.duolingo.unlocksuper.fingerprints.IsUserSuperMethodFingerprint +import app.revanced.patches.duolingo.unlocksuper.fingerprints.UserSerializationMethodFingerprint +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction22c +import com.android.tools.smali.dexlib2.iface.reference.Reference + +@Patch +@Name("Unlock Duolingo Super") +@Description("Unlocks Duolingo Super features.") +@Compatibility([Package("com.duolingo")]) +class UnlockDuolingoSuperPatch : BytecodePatch( + listOf(UserSerializationMethodFingerprint, IsUserSuperMethodFingerprint) +) { + + /* First find the reference to the isUserSuper field, then patch the instruction that assigns it to false. + * This strategy is used because the method that sets the isUserSuper field is difficult to fingerprint reliably. + */ + override fun execute(context: BytecodeContext) { + // Find the reference to the isUserSuper field. + val isUserSuperReference = IsUserSuperMethodFingerprint + .result + ?.mutableMethod + ?.getInstructions() + ?.filterIsInstance() + ?.firstOrNull { it.opcode == Opcode.IGET_BOOLEAN } + ?.reference + ?: throw IsUserSuperMethodFingerprint.toErrorResult() + + // Patch the instruction that assigns isUserSuper to true. + UserSerializationMethodFingerprint + .result + ?.mutableMethod + ?.apply { + replaceInstructions( + indexOfReference(isUserSuperReference) - 1, + "const/4 v2, 0x1" + ) + } + ?: throw UserSerializationMethodFingerprint.toErrorResult() + } + + private companion object { + private fun MutableMethod.indexOfReference(reference: Reference) = getInstructions() + .filterIsInstance() + .filter { it.opcode == Opcode.IPUT_BOOLEAN }.indexOfFirst { it.reference == reference }.let { + if (it == -1) throw PatchException("Could not find index of instruction with supplied reference.") + else it + } + } +} \ No newline at end of file