-
-
Notifications
You must be signed in to change notification settings - Fork 681
Patches for Continuum to pretend to be RedReader #6487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package app.revanced.patches.reddit.customclients.continuum.api | ||
|
|
||
| object Constants { | ||
| const val NEW_USER_AGENT = "org.quantumbadger.redreader/1.25.1" | ||
| const val OLD_REDIRECT_URI = "continuum://localhost" | ||
| const val NEW_REDIRECT_URI = "redreader://rr_oauth_redir" | ||
| const val OLD_CLIENT_ID = "Ro2J-y8bN412oS6BeaIj0A" | ||
| const val NEW_CLIENT_ID = "QnM1dlkC_2UfSlACOTXGRw" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package app.revanced.patches.reddit.customclients.continuum.api | ||
|
|
||
| import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction | ||
| import app.revanced.patcher.patch.bytecodePatch | ||
| import app.revanced.patches.all.misc.transformation.transformInstructionsPatch | ||
| import app.revanced.patches.reddit.customclients.continuum.misc.removeClientIdCheckPatch | ||
| import app.revanced.patches.shared.misc.string.replaceStringPatch | ||
| import com.android.tools.smali.dexlib2.Opcode | ||
| import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction | ||
| import com.android.tools.smali.dexlib2.iface.reference.StringReference | ||
|
|
||
| @Suppress("unused") | ||
| val redditApiBytecodePatch = bytecodePatch( | ||
| name = "Reddit API override", | ||
| description = "Overrides Reddit User-Agent, Redirect URI, and Client ID", | ||
| ) { | ||
| compatibleWith("org.cygnusx1.continuum", "org.cygnusx1.continuum.debug") | ||
|
|
||
| dependsOn( | ||
| redditApiResourcePatch, | ||
| // Use regex-based replacement for user agent to work across all versions | ||
| transformInstructionsPatch( | ||
| filterMap = filterMap@{ _, _, instruction, instructionIndex -> | ||
| if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) { | ||
| return@filterMap null | ||
| } | ||
|
|
||
| val stringReference = (instruction as? com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction) | ||
| ?.reference as? StringReference ?: return@filterMap null | ||
|
|
||
| val pattern = "android:org\\.cygnusx1\\.continuum:\\d+\\.\\d+\\.\\d+\\.\\d+ \\(by /u/edgan\\)".toRegex() | ||
| if (!pattern.matches(stringReference.string)) return@filterMap null | ||
|
|
||
| Triple(instructionIndex, instruction as OneRegisterInstruction, stringReference.string) | ||
| }, | ||
| transform = { mutableMethod, entry -> | ||
| val (instructionIndex, instruction, _) = entry | ||
| mutableMethod.replaceInstruction( | ||
| instructionIndex, | ||
| "${instruction.opcode.name} v${instruction.registerA}, \"${Constants.NEW_USER_AGENT}\"", | ||
| ) | ||
| }, | ||
| ), | ||
| replaceStringPatch(Constants.OLD_REDIRECT_URI, Constants.NEW_REDIRECT_URI), | ||
| removeClientIdCheckPatch | ||
| ) | ||
|
|
||
| execute { | ||
| // The actual replacements are handled by the dependencies: | ||
| // - redditApiResourcePatch: modifies default_client_id in res/values/strings.xml | ||
| // - transformInstructionsPatch: replaces user agent with regex pattern matching | ||
| // - replaceStringPatch: replaces hardcoded redirect URI strings | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package app.revanced.patches.reddit.customclients.continuum.api | ||
|
|
||
| import app.revanced.patcher.patch.resourcePatch | ||
| import app.revanced.util.findElementByAttributeValueOrThrow | ||
|
|
||
| val redditApiResourcePatch = resourcePatch( | ||
| name = "Reddit API override (resource)", | ||
| description = "Overrides Reddit client ID in strings.xml", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just patch the bytecode that obtains the default client id string |
||
| ) { | ||
| compatibleWith("org.cygnusx1.continuum", "org.cygnusx1.continuum.debug") | ||
|
|
||
| execute { | ||
| document("res/values/strings.xml").use { document -> | ||
| document.documentElement.childNodes.findElementByAttributeValueOrThrow( | ||
| "name", | ||
| "default_client_id" | ||
| ).textContent = Constants.NEW_CLIENT_ID | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| package app.revanced.patches.reddit.customclients.continuum.misc | ||
|
|
||
| import app.revanced.patcher.extensions.InstructionExtensions.getInstruction | ||
| import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions | ||
| import app.revanced.patcher.fingerprint | ||
| import app.revanced.patcher.patch.bytecodePatch | ||
| import app.revanced.util.getReference | ||
| import app.revanced.util.indexOfFirstInstructionOrThrow | ||
| import app.revanced.util.indexOfFirstInstructionReversedOrThrow | ||
| import com.android.tools.smali.dexlib2.Opcode | ||
| import com.android.tools.smali.dexlib2.iface.reference.MethodReference | ||
| import com.android.tools.smali.dexlib2.iface.reference.StringReference | ||
| import com.android.tools.smali.dexlib2.iface.reference.TypeReference | ||
|
|
||
| internal val clientIdCheckFingerprint = fingerprint { | ||
| strings("client_id_pref_key") | ||
| } | ||
|
|
||
| @Suppress("unused") | ||
| val removeClientIdCheckPatch = bytecodePatch( | ||
| name = "Remove client ID check", | ||
| description = "Removes the dialog that prevents login with default client ID", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The entire PR can be simplified a little. Instead of this, simply patch the code that actually makes the api calls or the place where the client id etc is saved. This way whatever the user enters, it would be overwritten by the patch.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The spoof client base patch can be adjusted to not just only spoof the client id, but also the redirect uri and user agent. Parametrize/adjust that patch accordingly. Then, implement it for continuum, done. |
||
| ) { | ||
| compatibleWith("org.cygnusx1.continuum", "org.cygnusx1.continuum.debug") | ||
|
|
||
| execute { | ||
| // Find all classes and methods containing "client_id_pref_key" | ||
| var targetMethod: com.android.tools.smali.dexlib2.iface.Method? = null | ||
| var targetClassDef: com.android.tools.smali.dexlib2.iface.ClassDef? = null | ||
|
|
||
| var methodsWithClientIdPrefKey = 0 | ||
| var methodsWithDialog = 0 | ||
|
|
||
| classes.forEach { classDef -> | ||
| classDef.methods.forEach { method -> | ||
| // Check if method contains "client_id_pref_key" string | ||
| val hasClientIdPrefKey = method.implementation?.instructions?.any { instruction -> | ||
| instruction.getReference<StringReference>()?.string == "client_id_pref_key" | ||
| } == true | ||
|
|
||
| if (hasClientIdPrefKey) { | ||
| methodsWithClientIdPrefKey++ | ||
|
|
||
| // Check if this method also has MaterialAlertDialogBuilder | ||
| val hasDialog = method.implementation?.instructions?.any { | ||
| it.opcode == Opcode.NEW_INSTANCE && | ||
| it.getReference<TypeReference>()?.toString()?.contains("MaterialAlertDialogBuilder") == true | ||
| } == true | ||
|
|
||
| if (hasDialog) { | ||
| methodsWithDialog++ | ||
| targetMethod = method | ||
| targetClassDef = classDef | ||
| return@forEach | ||
| } | ||
| } | ||
| } | ||
| if (targetMethod != null) return@forEach | ||
| } | ||
|
|
||
| if (targetMethod == null || targetClassDef == null) { | ||
| throw Exception("Could not find MainActivity method with client ID check") | ||
| } | ||
|
|
||
| // Now patch the method | ||
| val mutableMethod = proxy(targetClassDef).mutableClass | ||
| .methods.first { it.name == targetMethod!!.name && it.parameterTypes == targetMethod!!.parameterTypes } | ||
|
|
||
|
|
||
| mutableMethod.apply { | ||
| // First find where "client_id_pref_key" is loaded | ||
| val clientIdPrefKeyIndex = indexOfFirstInstructionOrThrow { | ||
| opcode == Opcode.CONST_STRING && | ||
| getReference<StringReference>()?.string == "client_id_pref_key" | ||
| } | ||
|
|
||
| // Find the String.equals call that comes AFTER the client_id_pref_key reference | ||
| val equalsIndex = indexOfFirstInstructionOrThrow(clientIdPrefKeyIndex) { | ||
| opcode == Opcode.INVOKE_VIRTUAL && | ||
| getReference<MethodReference>()?.toString()?.contains("Ljava/lang/String;->equals") == true | ||
| } | ||
|
|
||
| // The next instruction is move-result | ||
| val moveResultIndex = equalsIndex + 1 | ||
|
|
||
| // After that is if-eqz that branches on the equals result | ||
| val ifEqzIndex = moveResultIndex + 1 | ||
|
|
||
| // Verify that this if-eqz is followed by MaterialAlertDialogBuilder | ||
| // to ensure we're removing the right code | ||
| val dialogCheck = indexOfFirstInstructionOrThrow(ifEqzIndex) { | ||
| opcode == Opcode.NEW_INSTANCE && | ||
| getReference<TypeReference>()?.toString()?.contains("MaterialAlertDialogBuilder") == true | ||
| } | ||
|
|
||
| // Find the iget-object that loads this$0 - work backwards from equals | ||
| // Try to find it by looking for iget-object that references MainActivity | ||
| var igetObjectIndex = -1 | ||
| try { | ||
| igetObjectIndex = indexOfFirstInstructionReversedOrThrow(equalsIndex - 1) { | ||
| opcode == Opcode.IGET_OBJECT && | ||
| (toString().contains("this\$0") || toString().contains("MainActivity")) | ||
| } | ||
| } catch (e: Exception) { | ||
| // If we can't find MainActivity reference, just find the first iget-object | ||
| igetObjectIndex = indexOfFirstInstructionReversedOrThrow(equalsIndex - 1) { | ||
| opcode == Opcode.IGET_OBJECT | ||
| } | ||
| } | ||
|
|
||
| // Find where the normal flow resumes (new Intent creation for LoginActivity) | ||
| // We need to find the Intent that comes AFTER the dialog code | ||
| // Look for the MaterialAlertDialogBuilder first, then find Intent after it | ||
| val dialogBuilderIndex = indexOfFirstInstructionOrThrow(ifEqzIndex) { | ||
| opcode == Opcode.NEW_INSTANCE && | ||
| getReference<TypeReference>()?.toString()?.contains("MaterialAlertDialogBuilder") == true | ||
| } | ||
|
|
||
| val intentIndex = indexOfFirstInstructionOrThrow(dialogBuilderIndex) { | ||
| opcode == Opcode.NEW_INSTANCE && | ||
| getReference<TypeReference>()?.toString()?.contains("Landroid/content/Intent;") == true | ||
| } | ||
|
|
||
| // Double-check: look at a few instructions after to see if it's LoginActivity | ||
| val nextInstruction = getInstruction(intentIndex + 1) | ||
|
|
||
| // So remove from igetObjectIndex to intentIndex (exclusive) | ||
| val count = intentIndex - igetObjectIndex | ||
| removeInstructions(igetObjectIndex, count) | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.