Refactor Patcher

This commit is contained in:
Lucaskyy 2022-03-19 18:35:41 +01:00
parent 733fb6a3b8
commit a9e7f19d51
No known key found for this signature in database
GPG Key ID: 1530BFF96D1EEB89
17 changed files with 192 additions and 159 deletions

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
</component>
</project>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

@ -16,7 +16,6 @@ dependencies {
implementation("org.ow2.asm:asm-util:9.2") implementation("org.ow2.asm:asm-util:9.2")
implementation("org.ow2.asm:asm-tree:9.2") implementation("org.ow2.asm:asm-tree:9.2")
implementation("org.ow2.asm:asm-commons:9.2") implementation("org.ow2.asm:asm-commons:9.2")
implementation("com.google.code.gson:gson:2.9.0")
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
} }

View File

@ -1,2 +1 @@
rootProject.name = "ReVanced Patcher" rootProject.name = "revanced-patcher"

View File

@ -1,38 +0,0 @@
package net.revanced.patcher
import net.revanced.patcher.signature.model.Signature
import org.objectweb.asm.tree.InsnList
import org.objectweb.asm.tree.MethodNode
internal class MethodResolver(private val targetMethods: List<MethodNode>, private val signatures: List<Signature>) {
fun resolve(): MutableMap<String, MethodNode> {
val methods = mutableMapOf<String, MethodNode>()
for (signature in signatures) {
val method = targetMethods.firstOrNull { method ->
method.access == signature.accessors &&
signature.parameters.all { parameter ->
method.parameters.any { methodParameter ->
true //TODO check for parameter element type
}
} && method.instructions.scanFor(signature.opcodes)
} ?: continue
methods[signature.name] = method
}
return methods
}
}
//TODO: implement returning the index of the needle in the hay
private fun InsnList.scanFor(pattern: Array<Int>): Boolean {
for (i in 0 until this.size()) {
var occurrence = 0
while (i + occurrence < this.size()) {
if (this.get(i + occurrence).opcode != pattern.get(occurrence)) break
if (++occurrence >= pattern.size) return true
}
}
return false
}

View File

@ -2,54 +2,45 @@ package net.revanced.patcher
import net.revanced.patcher.cache.Cache import net.revanced.patcher.cache.Cache
import net.revanced.patcher.patch.Patch import net.revanced.patcher.patch.Patch
import net.revanced.patcher.signature.model.Signature import net.revanced.patcher.patch.PatchResult
import org.objectweb.asm.ClassReader import net.revanced.patcher.resolver.MethodResolver
import org.objectweb.asm.tree.ClassNode import net.revanced.patcher.signature.Signature
import org.objectweb.asm.tree.MethodNode import net.revanced.patcher.util.Jar2ASM
import java.io.File import java.io.InputStream
import java.util.jar.JarFile import java.util.jar.JarFile
class Patcher private constructor( /**
file: File, * The patcher. (docs WIP)
signatures: List<Signature> *
* @param input the input stream to read from, must be a JAR file (for now)
*/
class Patcher (
input: InputStream,
signatures: Array<Signature>,
) { ) {
val cache = Cache() val cache = Cache()
private val patches: MutableList<Patch> = mutableListOf() private val patches: MutableList<Patch> = mutableListOf()
init { init {
// collecting all methods here cache.methods.putAll(MethodResolver(Jar2ASM.jar2asm(input), signatures).resolve())
val targetMethods: MutableList<MethodNode> = mutableListOf()
val jarFile = JarFile(file)
jarFile.stream().forEach { jarEntry ->
jarFile.getInputStream(jarEntry).use { jis ->
if (jarEntry.name.endsWith(".class")) {
val classNode = ClassNode()
ClassReader(jis.readAllBytes()).accept(classNode, ClassReader.EXPAND_FRAMES)
targetMethods.addAll(classNode.methods)
}
}
}
// reducing to required methods via signatures
cache.Methods = MethodResolver(targetMethods, signatures).resolve()
}
companion object {
fun loadFromFile(file: String, signatures: List<Signature>): Patcher = Patcher(File(file), signatures)
} }
fun addPatches(vararg patches: Patch) { fun addPatches(vararg patches: Patch) {
this.patches.addAll(patches) this.patches.addAll(patches)
} }
fun executePatches(): String? { fun executePatches(): Map<String, Result<Nothing?>> {
for (patch in patches) { return buildMap {
val result = patch.execute() for (patch in patches) {
if (result.isSuccess()) continue val result: Result<Nothing?> = try {
return result.error()!!.errorMessage() val pr = patch.execute()
if (pr.isSuccess()) continue
Result.failure(Exception(pr.error()?.errorMessage() ?: "Unknown error"))
} catch (e: Exception) {
Result.failure(e)
}
this[patch.patchName] = result
}
} }
return null
} }
} }

View File

@ -1,7 +1,13 @@
package net.revanced.patcher.cache package net.revanced.patcher.cache
import org.objectweb.asm.tree.MethodNode
data class Cache( data class Cache(
var Methods: Map<String, MethodNode> = mutableMapOf() val methods: MethodMap = MethodMap()
) )
class MethodMap : LinkedHashMap<String, PatchData>() {
override fun get(key: String): PatchData {
return super.get(key) ?: throw MethodNotFoundException("Method $key not found in method cache")
}
}
class MethodNotFoundException(s: String) : Exception(s)

View File

@ -0,0 +1,9 @@
package net.revanced.patcher.cache
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
data class PatchData(
val cls: ClassNode,
val method: MethodNode
)

View File

@ -0,0 +1,67 @@
package net.revanced.patcher.resolver
import net.revanced.patcher.cache.PatchData
import net.revanced.patcher.signature.Signature
import org.objectweb.asm.Type
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.InsnList
import org.objectweb.asm.tree.MethodNode
internal class MethodResolver(private val classList: List<ClassNode>, private val signatures: Array<Signature>) {
fun resolve(): MutableMap<String, PatchData> {
val patchData = mutableMapOf<String, PatchData>()
for ((classNode, methods) in classList) {
for (method in methods) {
for (signature in signatures) {
if (patchData.containsKey(signature.name)) continue // method already found for this sig
if (!this.cmp(method, signature)) continue
patchData[signature.name] = PatchData(classNode, method)
}
}
}
for (signature in signatures) {
if (patchData.containsKey(signature.name)) continue
// not found
// TODO log error or whatever
}
return patchData
}
private fun cmp(method: MethodNode, signature: Signature): Boolean {
if (signature.returns != Type.getReturnType(method.desc)) return false
if (signature.accessors != method.access) return false
if (!signature.parameters.contentEquals(Type.getArgumentTypes(method.desc))) return false
val result = method.instructions.scanFor(signature.opcodes)
if (!result.found) return false
// TODO make use of the startIndex and endIndex we have from the result
return true
}
}
private operator fun ClassNode.component1(): ClassNode {
return this
}
private operator fun ClassNode.component2(): List<MethodNode> {
return this.methods
}
private fun InsnList.scanFor(pattern: Array<Int>): ScanResult {
for (i in 0 until this.size()) {
var occurrence = 0
while (i + occurrence < this.size()) {
val current = i + occurrence
if (this[current].opcode != pattern[occurrence]) break
if (++occurrence >= pattern.size) {
return ScanResult(true, current - pattern.size, current)
}
}
}
return ScanResult(false)
}

View File

@ -0,0 +1,7 @@
package net.revanced.patcher.resolver
internal data class ScanResult(
val found: Boolean,
val startIndex: Int? = 0,
val endIndex: Int? = 0
)

View File

@ -1,4 +1,4 @@
package net.revanced.patcher.signature.model package net.revanced.patcher.signature
import org.objectweb.asm.Type import org.objectweb.asm.Type
import org.objectweb.asm.tree.ParameterNode import org.objectweb.asm.tree.ParameterNode
@ -13,13 +13,14 @@ import org.objectweb.asm.tree.ParameterNode
* This method name will be used to find the corresponding patch. * This method name will be used to find the corresponding patch.
* @param returns The return type/signature of the method. * @param returns The return type/signature of the method.
* @param accessors The accessors of the method. * @param accessors The accessors of the method.
* @param parameters The parameter types/signatures of the method. * @param parameters The parameter types of the method.
* @param opcodes The opcode pattern of the method, used to find the method by signature scanning. * @param opcodes The opcode pattern of the method, used to find the method by pattern scanning.
*/ */
@Suppress("ArrayInDataClass")
data class Signature( data class Signature(
val name: String, val name: String,
val returns: Type, val returns: Type,
@Suppress("ArrayInDataClass") val accessors: Int, val accessors: Int,
@Suppress("ArrayInDataClass") val parameters: Array<ParameterNode>, val parameters: Array<Type>,
@Suppress("ArrayInDataClass") val opcodes: Array<Int> val opcodes: Array<Int>
) )

View File

@ -1,12 +0,0 @@
package net.revanced.patcher.signature
import com.google.gson.Gson
import net.revanced.patcher.signature.model.Signature
object SignatureLoader {
private val gson = Gson()
fun LoadFromJson(json: String): Array<Signature> {
return gson.fromJson(json, Array<Signature>::class.java)
}
}

View File

@ -0,0 +1,12 @@
package net.revanced.patcher.util
import org.objectweb.asm.Type
object ExtraTypes {
/**
* Any object type.
* Should be used instead of types such as: "Ljava/lang/String;"
*/
val Any = Type.getType(Object::class.java)
val ArrayAny = Type.getType(Array<String>::class.java)
}

View File

@ -0,0 +1,22 @@
package net.revanced.patcher.util
import org.objectweb.asm.ClassReader
import org.objectweb.asm.tree.ClassNode
import java.io.InputStream
import java.util.jar.JarInputStream
object Jar2ASM {
fun jar2asm(input: InputStream): List<ClassNode> {
return buildList {
val jar = JarInputStream(input)
while (true) {
val e = jar.nextJarEntry ?: break
if (e.name.endsWith(".class")) {
val classNode = ClassNode()
ClassReader(jar.readAllBytes()).accept(classNode, ClassReader.EXPAND_FRAMES)
this.add(classNode)
}
}
}
}
}

View File

@ -1,11 +0,0 @@
package net.revanced.patcher.util
import org.objectweb.asm.tree.ClassNode
class PatternScanner (private val classes: Array<ClassNode>) {
companion object {
fun scan(){
}
}
}

View File

@ -1,30 +1,47 @@
package net.revanced.patcher package net.revanced.patcher
import net.revanced.patcher.patch.Patch import net.revanced.patcher.patch.Patch
import net.revanced.patcher.patch.PatchResultError
import net.revanced.patcher.patch.PatchResultSuccess import net.revanced.patcher.patch.PatchResultSuccess
import net.revanced.patcher.signature.SignatureLoader import net.revanced.patcher.signature.Signature
import net.revanced.patcher.util.ExtraTypes
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.objectweb.asm.Opcodes.*
import org.objectweb.asm.Type
internal class PatcherTest { internal class PatcherTest {
@Test private val testSigs: Array<Signature> = arrayOf(
fun template() { Signature(
val patcher = Patcher.loadFromFile( "testMethod",
"some.apk", Type.BOOLEAN_TYPE,
SignatureLoader.LoadFromJson("signatures.json").toMutableList() ACC_PUBLIC or ACC_STATIC,
arrayOf(
ExtraTypes.ArrayAny,
),
arrayOf(
GETSTATIC,
LDC,
INVOKEVIRTUAL
)
) )
)
val patches = mutableListOf( @Test
Patch ("RemoveVideoAds") { fun testPatcher() {
val videoAdShowMethodInstr = patcher.cache.Methods["SomeMethod"]?.instructions val testData = PatcherTest::class.java.getResourceAsStream("/test1.jar")!!
val patcher = Patcher(testData, testSigs)
patcher.addPatches(
Patch ("TestPatch") {
patcher.cache.methods["testMethod"]
PatchResultSuccess() PatchResultSuccess()
},
Patch ("TweakLayout") {
val layoutMethod = patcher.cache.Methods["SomeMethod2"]
PatchResultError("Failed")
} }
) )
patcher.executePatches() val result = patcher.executePatches()
for ((s, r) in result) {
if (r.isFailure) {
throw Exception("Patch $s failed", r.exceptionOrNull()!!)
}
}
} }
} }