diff --git a/deob/src/main/java/dev/openrs2/deob/remap/StaticClassGenerator.kt b/deob/src/main/java/dev/openrs2/deob/remap/StaticClassGenerator.kt new file mode 100644 index 00000000..18243f12 --- /dev/null +++ b/deob/src/main/java/dev/openrs2/deob/remap/StaticClassGenerator.kt @@ -0,0 +1,20 @@ +package dev.openrs2.deob.remap + +class StaticClassGenerator(private val generator: NameGenerator, private val maxMembers: Int) { + private var lastClass: String? = null + private var members = 0 + + fun generate(): String { + var clazz = lastClass + + if (clazz == null || members >= maxMembers) { + clazz = generator.generate("Static") + lastClass = clazz + members = 1 + } else { + members++ + } + + return clazz + } +} diff --git a/deob/src/main/java/dev/openrs2/deob/remap/StaticField.kt b/deob/src/main/java/dev/openrs2/deob/remap/StaticField.kt new file mode 100644 index 00000000..ccc2b206 --- /dev/null +++ b/deob/src/main/java/dev/openrs2/deob/remap/StaticField.kt @@ -0,0 +1,5 @@ +package dev.openrs2.deob.remap + +import org.objectweb.asm.tree.AbstractInsnNode + +class StaticField(val owner: String, val initializer: List?) diff --git a/deob/src/main/java/dev/openrs2/deob/remap/StaticFieldUnscrambler.kt b/deob/src/main/java/dev/openrs2/deob/remap/StaticFieldUnscrambler.kt new file mode 100644 index 00000000..2038a1ae --- /dev/null +++ b/deob/src/main/java/dev/openrs2/deob/remap/StaticFieldUnscrambler.kt @@ -0,0 +1,119 @@ +package dev.openrs2.deob.remap + +import dev.openrs2.asm.MemberDesc +import dev.openrs2.asm.MemberRef +import dev.openrs2.asm.classpath.ClassPath +import dev.openrs2.asm.filter.MemberFilter +import dev.openrs2.asm.getExpression +import dev.openrs2.asm.isSequential +import dev.openrs2.util.collect.DisjointSet +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.FieldInsnNode +import org.objectweb.asm.tree.MethodNode + +class StaticFieldUnscrambler( + private val classPath: ClassPath, + private val excludedFields: MemberFilter, + private val inheritedFieldSets: DisjointSet, + staticClassNameGenerator: NameGenerator +) { + private val generator = StaticClassGenerator(staticClassNameGenerator, MAX_FIELDS_PER_CLASS) + + fun unscramble(): Map, StaticField> { + val fields = mutableMapOf, StaticField>() + + for (library in classPath.libraries) { + if ("client" !in library) { + // TODO(gpe): improve detection of the client library + continue + } + + for (clazz in library) { + // TODO(gpe): exclude the JSObject class + + val clinit = clazz.methods.find { it.name == "" } + val (simpleInitializers, complexInitializers) = clinit?.extractInitializers(clazz.name) + ?: Pair(emptyMap(), emptySet()) + + for (field in clazz.fields) { + if (field.access and Opcodes.ACC_STATIC == 0) { + continue + } else if (excludedFields.matches(clazz.name, field.name, field.desc)) { + continue + } + + val desc = MemberDesc(field) + if (complexInitializers.contains(desc)) { + continue + } + + val member = MemberRef(clazz, field) + val partition = inheritedFieldSets[member]!! + fields[partition] = StaticField(generator.generate(), simpleInitializers[desc]) + } + } + } + + return fields + } + + private fun MethodNode.extractEntryExitBlocks(): List { + /* + * Most (or all?) of the methods have "simple" initializers + * that we're capable of moving in the first and last basic blocks of + * the method. The last basic block is always at the end of the code + * and ends in a RETURN. This allows us to avoid worrying about making + * a full basic block control flow graph here. + */ + val entry = instructions.takeWhile { it.isSequential } + + val last = instructions.lastOrNull() + if (last == null || last.opcode != Opcodes.RETURN) { + return entry + } + + val exit = instructions.toList() + .dropLast(1) + .takeLastWhile { it.isSequential } + + return entry.plus(exit) + } + + private fun MethodNode.extractInitializers( + owner: String + ): Pair>, Set> { + val entryExitBlocks = extractEntryExitBlocks() + + val simpleInitializers = mutableMapOf>() + val complexInitializers = instructions.asSequence() + .filter { !entryExitBlocks.contains(it) } + .filterIsInstance() + .filter { it.opcode == Opcodes.GETSTATIC && it.owner == owner } + .filter { !excludedFields.matches(it.owner, it.name, it.desc) } + .map(::MemberDesc) + .toSet() + + val putstatics = entryExitBlocks + .filterIsInstance() + .filter { it.opcode == Opcodes.PUTSTATIC && it.owner == owner } + .filter { !excludedFields.matches(it.owner, it.name, it.desc) } + + for (putstatic in putstatics) { + val desc = MemberDesc(putstatic) + if (simpleInitializers.containsKey(desc) || complexInitializers.contains(desc)) { + continue + } + + // TODO(gpe): use a filter here (pure with no *LOADs?) + val expr = getExpression(putstatic) ?: continue + simpleInitializers[desc] = expr.plus(putstatic) + } + + return Pair(simpleInitializers, complexInitializers) + } + + private companion object { + private const val MAX_FIELDS_PER_CLASS = 500 + } +} diff --git a/deob/src/main/java/dev/openrs2/deob/remap/StaticMethodUnscrambler.kt b/deob/src/main/java/dev/openrs2/deob/remap/StaticMethodUnscrambler.kt new file mode 100644 index 00000000..eff836eb --- /dev/null +++ b/deob/src/main/java/dev/openrs2/deob/remap/StaticMethodUnscrambler.kt @@ -0,0 +1,51 @@ +package dev.openrs2.deob.remap + +import dev.openrs2.asm.MemberRef +import dev.openrs2.asm.classpath.ClassPath +import dev.openrs2.asm.filter.MemberFilter +import dev.openrs2.util.collect.DisjointSet +import org.objectweb.asm.Opcodes + +class StaticMethodUnscrambler( + private val classPath: ClassPath, + private val excludedMethods: MemberFilter, + private val inheritedMethodSets: DisjointSet, + staticClassNameGenerator: NameGenerator +) { + private val generator = StaticClassGenerator(staticClassNameGenerator, MAX_METHODS_PER_CLASS) + + fun unscramble(): Map, String> { + val owners = mutableMapOf, String>() + + for (library in classPath.libraries) { + if ("client" !in library) { + // TODO(gpe): improve detection of the client library + continue + } + + for (clazz in library) { + // TODO(gpe): exclude the JSObject class + + for (method in clazz.methods) { + if (method.access and Opcodes.ACC_STATIC == 0) { + continue + } else if (method.access and Opcodes.ACC_NATIVE != 0) { + continue + } else if (excludedMethods.matches(clazz.name, method.name, method.desc)) { + continue + } + + val member = MemberRef(clazz, method) + val partition = inheritedMethodSets[member]!! + owners[partition] = generator.generate() + } + } + } + + return owners + } + + private companion object { + private const val MAX_METHODS_PER_CLASS = 50 + } +} diff --git a/deob/src/main/java/dev/openrs2/deob/remap/TypedRemapper.kt b/deob/src/main/java/dev/openrs2/deob/remap/TypedRemapper.kt index 6a4fe62d..e8b5b282 100644 --- a/deob/src/main/java/dev/openrs2/deob/remap/TypedRemapper.kt +++ b/deob/src/main/java/dev/openrs2/deob/remap/TypedRemapper.kt @@ -6,13 +6,16 @@ import dev.openrs2.asm.classpath.ClassPath import dev.openrs2.asm.classpath.ExtendedRemapper import dev.openrs2.deob.Profile import dev.openrs2.util.collect.DisjointSet +import org.objectweb.asm.tree.AbstractInsnNode class TypedRemapper private constructor( private val inheritedFieldSets: DisjointSet, private val inheritedMethodSets: DisjointSet, private val classes: Map, private val fields: Map, String>, - private val methods: Map, String> + private val methods: Map, String>, + private val staticFields: Map, StaticField>, + private val staticMethods: Map, String> ) : ExtendedRemapper() { override fun map(internalName: String): String { return classes.getOrDefault(internalName, internalName) @@ -24,12 +27,30 @@ class TypedRemapper private constructor( return fields.getOrDefault(partition, name) } + override fun mapFieldOwner(owner: String, name: String, descriptor: String): String { + val member = MemberRef(owner, name, descriptor) + val partition = inheritedFieldSets[member] ?: return mapType(owner) + return staticFields[partition]?.owner ?: mapType(owner) + } + + override fun getFieldInitializer(owner: String, name: String, descriptor: String): List? { + val member = MemberRef(owner, name, descriptor) + val partition = inheritedFieldSets[member] ?: return null + return staticFields[partition]?.initializer + } + override fun mapMethodName(owner: String, name: String, descriptor: String): String { val member = MemberRef(owner, name, descriptor) val partition = inheritedMethodSets[member] ?: return name return methods.getOrDefault(partition, name) } + override fun mapMethodOwner(owner: String, name: String, descriptor: String): String { + val member = MemberRef(owner, name, descriptor) + val partition = inheritedMethodSets[member] ?: return mapType(owner) + return staticMethods.getOrDefault(partition, mapType(owner)) + } + companion object { private val logger = InlineLogger() @@ -56,7 +77,29 @@ class TypedRemapper private constructor( verifyMemberMapping(fields, profile.maxObfuscatedNameLen) verifyMemberMapping(methods, profile.maxObfuscatedNameLen) - return TypedRemapper(inheritedFieldSets, inheritedMethodSets, classes, fields, methods) + val staticClassNameGenerator = NameGenerator() + val staticFields = StaticFieldUnscrambler( + classPath, + profile.excludedFields, + inheritedFieldSets, + staticClassNameGenerator + ).unscramble() + val staticMethods = StaticMethodUnscrambler( + classPath, + profile.excludedMethods, + inheritedMethodSets, + staticClassNameGenerator + ).unscramble() + + return TypedRemapper( + inheritedFieldSets, + inheritedMethodSets, + classes, + fields, + methods, + staticFields, + staticMethods + ) } private fun verifyMapping(mapping: Map, maxObfuscatedNameLen: Int) {