diff --git a/api/revanced-patches.api b/api/revanced-patches.api index 805eda8be8..05238405da 100644 --- a/api/revanced-patches.api +++ b/api/revanced-patches.api @@ -193,6 +193,12 @@ public final class app/revanced/patches/cieid/restrictions/root/BypassRootChecks public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V } +public final class app/revanced/patches/duolingo/unlocksuper/UnlockDuolingoSuperPatch : app/revanced/patcher/patch/BytecodePatch { + public static final field INSTANCE Lapp/revanced/patches/duolingo/unlocksuper/UnlockDuolingoSuperPatch; + public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V + public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V +} + public final class app/revanced/patches/facebook/ads/story/HideStoryAdsPatch : app/revanced/patcher/patch/BytecodePatch { public static final field INSTANCE Lapp/revanced/patches/facebook/ads/story/HideStoryAdsPatch; public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V diff --git a/src/main/kotlin/app/revanced/patches/duolingo/unlocksuper/UnlockDuolingoSuperPatch.kt b/src/main/kotlin/app/revanced/patches/duolingo/unlocksuper/UnlockDuolingoSuperPatch.kt new file mode 100644 index 0000000000..dc3ab95d47 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/duolingo/unlocksuper/UnlockDuolingoSuperPatch.kt @@ -0,0 +1,74 @@ +package app.revanced.patches.duolingo.unlocksuper + +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstructions +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.annotation.CompatiblePackage +import app.revanced.patcher.patch.annotation.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 app.revanced.util.exception +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction22c +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.Reference + +@Patch( + name = "Unlock Duolingo Super", + compatiblePackages = [CompatiblePackage("com.duolingo")] +) +@Suppress("unused") +object UnlockDuolingoSuperPatch : BytecodePatch( + setOf( + 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.exception + + // Patch the instruction that assigns isUserSuper to true. + UserSerializationMethodFingerprint + .result + ?.mutableMethod + ?.apply { + val assignIndex = indexOfReference(isUserSuperReference) + val assignInstruction = getInstruction(assignIndex) + + // add an instruction to force the value to `true`. ideally we'd replace the existing + // instruction, but there's an `if` block above with different paths based on various + // states (i.e. subscription vs super vs gold or whatever), and I don't think it's + // worth removing the entire statement. + addInstructions( + assignIndex + 1, + """ + const/4 v${assignInstruction.registerA}, 0x1 + iput-boolean v${assignInstruction.registerA}, v0, $isUserSuperReference + """.trimIndent() + ) + } + ?: throw UserSerializationMethodFingerprint.exception + } + + private fun MutableMethod.indexOfReference(reference: Reference) = getInstructions() + .indexOfFirst { it is BuilderInstruction22c && it.opcode == Opcode.IPUT_BOOLEAN && 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 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 0000000000..ed5d20a8e3 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/duolingo/unlocksuper/fingerprints/IsUserSuperMethodFingerprint.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.duolingo.unlocksuper.fingerprints + +import app.revanced.patcher.extensions.or +import app.revanced.patcher.fingerprint.MethodFingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal object IsUserSuperMethodFingerprint : MethodFingerprint( + returnType = "Ljava/lang/Object", + parameters = listOf( + "Ljava/lang/Object", + "Ljava/lang/Object", + ), + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + strings = listOf( + "user", + "heartsState", + "superData", + ), + 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 0000000000..f52372c123 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/duolingo/unlocksuper/fingerprints/UserSerializationMethodFingerprint.kt @@ -0,0 +1,19 @@ +package app.revanced.patches.duolingo.unlocksuper.fingerprints + +import app.revanced.patcher.extensions.or +import app.revanced.patcher.fingerprint.MethodFingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal object UserSerializationMethodFingerprint : MethodFingerprint( + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + strings = listOf( + "betaStatus", + "subscriberLevel", + ), + opcodes = listOf( + Opcode.MOVE_FROM16, + Opcode.IPUT_BOOLEAN, + ), +) \ No newline at end of file