Open-source multiplayer game server compatible with the RuneScape client https://www.openrs2.org/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openrs2/deob/src/main/java/dev/openrs2/deob/transform/StaticScramblingTransformer.kt

222 lines
8.2 KiB

package dev.openrs2.deob.transform
import com.github.michaelbull.logging.InlineLogger
import dev.openrs2.asm.ClassVersionUtils
import dev.openrs2.asm.MemberRef
import dev.openrs2.asm.classpath.ClassPath
import dev.openrs2.asm.classpath.Library
import dev.openrs2.asm.transform.Transformer
Replace TypedRemapper constants with a more flexible system The new system will make it easier to port the deobfuscator to different revisions. There are two main changes: - The addition of a Profile class, which contains a list of excluded classes, methods and fields, and the maximum obfuscated name length. It is passed to Transformers that require it with dependency injection. - New ClassFilter and MemberFilter infrastructure. The MemberFilter class adds support for filtering fields and methods based on the owner and descriptor, in addition to the name. This makes the filters more precise than the previous system. It also supports globs, which makes it easy to filter whole groups of classes, fields and methods in one go. The Profile class uses a ClassFilter and MemberFilters to represent the list of excluded classes, methods and fields. A separate benefit is the addition of a separate entry points filter to the Profile class. Prior to this commit, many Transformers re-used the excluded method filter to find entry points, which is less precise (many of the excluded methods in 550 are not entry points). Support for filtering methods by owner and descriptor in addition to name allows the DEFAULT_PUBLIC_CTOR_CLASSES Set in VisibilityTransformer to be combined with the entry points filter. In the future it might be desirable to split the excluded method set into three separate sets: - One to represent methods that can't be renamed. - One to represent methods whose signature can't be changed. - One to represent methods that can't be removed. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
import dev.openrs2.deob.Profile
import dev.openrs2.util.collect.DisjointSet
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.FieldInsnNode
import org.objectweb.asm.tree.FieldNode
import org.objectweb.asm.tree.InsnList
import org.objectweb.asm.tree.InsnNode
import org.objectweb.asm.tree.JumpInsnNode
import org.objectweb.asm.tree.LabelNode
import org.objectweb.asm.tree.MethodInsnNode
import org.objectweb.asm.tree.MethodNode
Replace TypedRemapper constants with a more flexible system The new system will make it easier to port the deobfuscator to different revisions. There are two main changes: - The addition of a Profile class, which contains a list of excluded classes, methods and fields, and the maximum obfuscated name length. It is passed to Transformers that require it with dependency injection. - New ClassFilter and MemberFilter infrastructure. The MemberFilter class adds support for filtering fields and methods based on the owner and descriptor, in addition to the name. This makes the filters more precise than the previous system. It also supports globs, which makes it easy to filter whole groups of classes, fields and methods in one go. The Profile class uses a ClassFilter and MemberFilters to represent the list of excluded classes, methods and fields. A separate benefit is the addition of a separate entry points filter to the Profile class. Prior to this commit, many Transformers re-used the excluded method filter to find entry points, which is less precise (many of the excluded methods in 550 are not entry points). Support for filtering methods by owner and descriptor in addition to name allows the DEFAULT_PUBLIC_CTOR_CLASSES Set in VisibilityTransformer to be combined with the entry points filter. In the future it might be desirable to split the excluded method set into three separate sets: - One to represent methods that can't be renamed. - One to represent methods whose signature can't be changed. - One to represent methods that can't be removed. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.max
@Singleton
Replace TypedRemapper constants with a more flexible system The new system will make it easier to port the deobfuscator to different revisions. There are two main changes: - The addition of a Profile class, which contains a list of excluded classes, methods and fields, and the maximum obfuscated name length. It is passed to Transformers that require it with dependency injection. - New ClassFilter and MemberFilter infrastructure. The MemberFilter class adds support for filtering fields and methods based on the owner and descriptor, in addition to the name. This makes the filters more precise than the previous system. It also supports globs, which makes it easy to filter whole groups of classes, fields and methods in one go. The Profile class uses a ClassFilter and MemberFilters to represent the list of excluded classes, methods and fields. A separate benefit is the addition of a separate entry points filter to the Profile class. Prior to this commit, many Transformers re-used the excluded method filter to find entry points, which is less precise (many of the excluded methods in 550 are not entry points). Support for filtering methods by owner and descriptor in addition to name allows the DEFAULT_PUBLIC_CTOR_CLASSES Set in VisibilityTransformer to be combined with the entry points filter. In the future it might be desirable to split the excluded method set into three separate sets: - One to represent methods that can't be renamed. - One to represent methods whose signature can't be changed. - One to represent methods that can't be removed. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
class StaticScramblingTransformer @Inject constructor(private val profile: Profile) : Transformer() {
private class FieldSet(val owner: ClassNode, val fields: List<FieldNode>, val clinit: MethodNode?) {
val dependencies = clinit?.instructions
?.filterIsInstance<FieldInsnNode>()
?.filter { it.opcode == Opcodes.GETSTATIC && it.owner != owner.name }
?.mapTo(mutableSetOf(), ::MemberRef) ?: emptySet<MemberRef>()
}
private lateinit var inheritedFieldSets: DisjointSet<MemberRef>
private lateinit var inheritedMethodSets: DisjointSet<MemberRef>
private val fieldSets = mutableMapOf<MemberRef, FieldSet>()
private val fieldClasses = mutableMapOf<DisjointSet.Partition<MemberRef>, String>()
private val methodClasses = mutableMapOf<DisjointSet.Partition<MemberRef>, String>()
private var nextStaticClass: ClassNode? = null
private var nextClinit: MethodNode? = null
private val staticClasses = mutableListOf<ClassNode>()
private fun nextClass(): Pair<ClassNode, MethodNode> {
var clazz = nextStaticClass
if (clazz != null && clazz.fields.size < MAX_FIELDS && clazz.methods.size < MAX_METHODS) {
return Pair(clazz, nextClinit!!)
}
val clinit = MethodNode()
clinit.access = Opcodes.ACC_STATIC
clinit.name = "<clinit>"
clinit.desc = "()V"
clinit.exceptions = mutableListOf()
clinit.parameters = mutableListOf()
clinit.instructions = InsnList()
clinit.instructions.add(InsnNode(Opcodes.RETURN))
clinit.tryCatchBlocks = mutableListOf()
clazz = ClassNode()
clazz.version = Opcodes.V1_1
clazz.access = Opcodes.ACC_PUBLIC or Opcodes.ACC_SUPER or Opcodes.ACC_FINAL
clazz.name = "Static${staticClasses.size + 1}"
clazz.superName = "java/lang/Object"
clazz.interfaces = mutableListOf()
clazz.innerClasses = mutableListOf()
clazz.fields = mutableListOf()
clazz.methods = mutableListOf(clinit)
staticClasses += clazz
nextStaticClass = clazz
nextClinit = clinit
return Pair(clazz, clinit)
}
private fun spliceFields() {
val done = mutableSetOf<FieldSet>()
for (fieldSet in fieldSets.values) {
spliceFields(done, fieldSet)
}
}
private fun spliceFields(done: MutableSet<FieldSet>, fieldSet: FieldSet) {
if (!done.add(fieldSet)) {
return
}
for (dependency in fieldSet.dependencies) {
val dependencyFieldSet = fieldSets[dependency] ?: continue
spliceFields(done, dependencyFieldSet)
}
val (staticClass, staticClinit) = nextClass()
staticClass.fields.addAll(fieldSet.fields)
staticClass.version = ClassVersionUtils.max(staticClass.version, fieldSet.owner.version)
if (fieldSet.clinit != null) {
// remove tail RETURN
val insns = fieldSet.clinit.instructions
val last = insns.lastOrNull()
if (last != null && last.opcode == Opcodes.RETURN) {
insns.remove(last)
}
// replace any remaining RETURNs with a GOTO to the end of the method
val end = LabelNode()
insns.add(end)
for (insn in insns) {
if (insn.opcode == Opcodes.RETURN) {
insns.set(insn, JumpInsnNode(Opcodes.GOTO, end))
}
}
// append just before the end of the static <clinit> RETURN
staticClinit.instructions.insertBefore(staticClinit.instructions.last, insns)
staticClinit.tryCatchBlocks.addAll(fieldSet.clinit.tryCatchBlocks)
staticClinit.maxStack = max(staticClinit.maxStack, fieldSet.clinit.maxStack)
staticClinit.maxLocals = max(staticClinit.maxLocals, fieldSet.clinit.maxLocals)
}
for (field in fieldSet.fields) {
val partition = inheritedFieldSets[MemberRef(fieldSet.owner, field)]!!
fieldClasses[partition] = staticClass.name
}
}
override fun preTransform(classPath: ClassPath) {
inheritedFieldSets = classPath.createInheritedFieldSets()
inheritedMethodSets = classPath.createInheritedMethodSets()
fieldSets.clear()
fieldClasses.clear()
methodClasses.clear()
nextStaticClass = null
staticClasses.clear()
for (library in classPath.libraries) {
// TODO(gpe): improve detection of the client library
if ("client" !in library) {
continue
}
for (clazz in library) {
// TODO(gpe): exclude the JSObject class
if (clazz.name == "jagex3/jagmisc/jagmisc") {
continue
}
val fields = clazz.fields.filter { it.access and Opcodes.ACC_STATIC != 0 }
clazz.fields.removeAll(fields)
val clinit = clazz.methods.find { it.name == "<clinit>" }
if (clinit != null) {
clazz.methods.remove(clinit)
}
val fieldSet = FieldSet(clazz, fields, clinit)
for (field in fields) {
val ref = MemberRef(clazz, field)
fieldSets[ref] = fieldSet
}
clazz.methods.removeIf { method ->
if (method.access and Opcodes.ACC_STATIC == 0) {
return@removeIf false
} else if (method.access and Opcodes.ACC_NATIVE != 0) {
return@removeIf false
Replace TypedRemapper constants with a more flexible system The new system will make it easier to port the deobfuscator to different revisions. There are two main changes: - The addition of a Profile class, which contains a list of excluded classes, methods and fields, and the maximum obfuscated name length. It is passed to Transformers that require it with dependency injection. - New ClassFilter and MemberFilter infrastructure. The MemberFilter class adds support for filtering fields and methods based on the owner and descriptor, in addition to the name. This makes the filters more precise than the previous system. It also supports globs, which makes it easy to filter whole groups of classes, fields and methods in one go. The Profile class uses a ClassFilter and MemberFilters to represent the list of excluded classes, methods and fields. A separate benefit is the addition of a separate entry points filter to the Profile class. Prior to this commit, many Transformers re-used the excluded method filter to find entry points, which is less precise (many of the excluded methods in 550 are not entry points). Support for filtering methods by owner and descriptor in addition to name allows the DEFAULT_PUBLIC_CTOR_CLASSES Set in VisibilityTransformer to be combined with the entry points filter. In the future it might be desirable to split the excluded method set into three separate sets: - One to represent methods that can't be renamed. - One to represent methods whose signature can't be changed. - One to represent methods that can't be removed. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
} else if (profile.excludedMethods.matches(clazz.name, method.name, method.desc)) {
return@removeIf false
}
val (staticClass, _) = nextClass()
staticClass.methods.add(method)
staticClass.version = ClassVersionUtils.max(staticClass.version, clazz.version)
val partition = inheritedMethodSets[MemberRef(clazz, method)]!!
methodClasses[partition] = staticClass.name
return@removeIf true
}
}
spliceFields()
for (clazz in staticClasses) {
library.add(clazz)
}
}
}
override fun transformCode(classPath: ClassPath, library: Library, clazz: ClassNode, method: MethodNode): Boolean {
for (insn in method.instructions) {
when (insn) {
is FieldInsnNode -> {
val partition = inheritedFieldSets[MemberRef(insn)]
if (partition != null) {
insn.owner = fieldClasses.getOrDefault(partition, insn.owner)
}
}
is MethodInsnNode -> {
val partition = inheritedMethodSets[MemberRef(insn)]
if (partition != null) {
insn.owner = methodClasses.getOrDefault(partition, insn.owner)
}
}
}
}
return false
}
override fun postTransform(classPath: ClassPath) {
logger.info { "Moved ${fieldClasses.size} fields and ${methodClasses.size} methods" }
}
private companion object {
private val logger = InlineLogger()
private const val MAX_FIELDS = 500
private const val MAX_METHODS = 50
}
}