forked from openrs2/openrs2
This change paves the way to feed the NameMap into the TypedRemapper, such that refactored names (and static member movements) are preserved across deobfuscator runs. Signed-off-by: Graham <gpe@openrs2.dev>
parent
cccbbe4d19
commit
7d9b1c61e8
@ -0,0 +1,219 @@ |
|||||||
|
package dev.openrs2.asm.classpath |
||||||
|
|
||||||
|
import dev.openrs2.asm.ClassVersionUtils |
||||||
|
import dev.openrs2.asm.MemberRef |
||||||
|
import dev.openrs2.asm.remap |
||||||
|
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.MethodNode |
||||||
|
import java.util.SortedMap |
||||||
|
import java.util.TreeMap |
||||||
|
import kotlin.math.max |
||||||
|
|
||||||
|
class LibraryRemapper( |
||||||
|
private val remapper: ExtendedRemapper, |
||||||
|
private var classes: SortedMap<String, ClassNode> |
||||||
|
) { |
||||||
|
private class Initializer(val instructions: InsnList, val maxStack: Int) { |
||||||
|
val dependencies = instructions.asSequence() |
||||||
|
.filterIsInstance<FieldInsnNode>() |
||||||
|
.filter { it.opcode == Opcodes.GETSTATIC } |
||||||
|
.map(::MemberRef) |
||||||
|
.toSet() |
||||||
|
} |
||||||
|
|
||||||
|
private class Field(val owner: String, val node: FieldNode, val version: Int, val initializer: Initializer?) |
||||||
|
private class Method(val owner: String, val node: MethodNode, val version: Int) |
||||||
|
|
||||||
|
private val fields = mutableMapOf<MemberRef, Field>() |
||||||
|
private val splicedFields = mutableSetOf<MemberRef>() |
||||||
|
private val methods = mutableListOf<Method>() |
||||||
|
|
||||||
|
fun remap(): SortedMap<String, ClassNode> { |
||||||
|
// extract static fields/methods that are being moved between classes |
||||||
|
extractFields() |
||||||
|
extractMethods() |
||||||
|
|
||||||
|
// map remaining fields/methods |
||||||
|
for (clazz in classes.values) { |
||||||
|
clazz.remap(remapper) |
||||||
|
} |
||||||
|
|
||||||
|
classes = classes.mapKeysTo(TreeMap()) { (_, clazz) -> clazz.name } |
||||||
|
|
||||||
|
// splice static fields/methods into their new classes |
||||||
|
spliceFields() |
||||||
|
spliceMethods() |
||||||
|
|
||||||
|
// remove empty <clinit> methods (so EmptyClassTransformer works later) |
||||||
|
removeEmptyClinitMethods() |
||||||
|
|
||||||
|
return classes |
||||||
|
} |
||||||
|
|
||||||
|
private fun extractFields() { |
||||||
|
for (clazz in classes.values) { |
||||||
|
clazz.fields.removeIf { field -> |
||||||
|
// do nothing if the field is not moved between classes |
||||||
|
val oldOwner = remapper.mapType(clazz.name) |
||||||
|
val newOwner = remapper.mapFieldOwner(clazz.name, field.name, field.desc) |
||||||
|
if (oldOwner == newOwner) { |
||||||
|
return@removeIf false |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
* Remove the initializer (if present) from the old owner's |
||||||
|
* <clinit> method. |
||||||
|
*/ |
||||||
|
val initializer = extractInitializer(clazz, field) |
||||||
|
|
||||||
|
/* |
||||||
|
* Map the field (it won't be caught by a ClassNode::remap call |
||||||
|
* during the main pass). |
||||||
|
*/ |
||||||
|
field.remap(remapper, clazz.name) |
||||||
|
|
||||||
|
/* |
||||||
|
* Store the field so it can be spliced into its new class |
||||||
|
* later. We key on the new owner/name/descriptor, rather than |
||||||
|
* the old one, as spliceFields's dependency tracking runs |
||||||
|
* after the Initializer::dependencies has been mapped. |
||||||
|
*/ |
||||||
|
val newMember = MemberRef(newOwner, field.name, field.desc) |
||||||
|
fields[newMember] = Field(newOwner, field, clazz.version, initializer) |
||||||
|
|
||||||
|
// remove the field from its old class |
||||||
|
return@removeIf true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun extractInitializer(clazz: ClassNode, field: FieldNode): Initializer? { |
||||||
|
val clinit = clazz.methods.find { it.name == "<clinit>" } ?: return null |
||||||
|
val initializer = remapper.getFieldInitializer(clazz.name, field.name, field.desc) ?: return null |
||||||
|
|
||||||
|
val list = InsnList() |
||||||
|
for (insn in initializer) { |
||||||
|
/* |
||||||
|
* Remove initializer from <clinit>. It is stored alongside the |
||||||
|
* FieldNode so it can be spliced into its new class later. |
||||||
|
*/ |
||||||
|
clinit.instructions.remove(insn) |
||||||
|
list.add(insn) |
||||||
|
|
||||||
|
/* |
||||||
|
* Remap the initializer (it won't be caught by a ClassNode::remap |
||||||
|
* call during the main pass). |
||||||
|
*/ |
||||||
|
insn.remap(remapper) |
||||||
|
} |
||||||
|
|
||||||
|
return Initializer(list, clinit.maxStack) |
||||||
|
} |
||||||
|
|
||||||
|
private fun extractMethods() { |
||||||
|
for (clazz in classes.values) { |
||||||
|
clazz.methods.removeIf { method -> |
||||||
|
// do nothing if the method is not moved between classes |
||||||
|
val oldOwner = remapper.mapType(clazz.name) |
||||||
|
val newOwner = remapper.mapMethodOwner(clazz.name, method.name, method.desc) |
||||||
|
if (oldOwner == newOwner) { |
||||||
|
return@removeIf false |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
* Map the method (it won't be caught by a ClassNode::remap call |
||||||
|
* during the main pass). |
||||||
|
*/ |
||||||
|
method.remap(remapper, clazz.name) |
||||||
|
|
||||||
|
// store the method |
||||||
|
methods += Method(newOwner, method, clazz.version) |
||||||
|
|
||||||
|
// remove the method from its old class |
||||||
|
return@removeIf true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun spliceFields() { |
||||||
|
for (member in fields.keys) { |
||||||
|
spliceField(member) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun spliceField(member: MemberRef) { |
||||||
|
if (!splicedFields.add(member)) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
val field = fields[member] ?: return |
||||||
|
val clazz = classes.computeIfAbsent(field.owner, ::createClass) |
||||||
|
|
||||||
|
if (field.initializer != null) { |
||||||
|
for (dependency in field.initializer.dependencies) { |
||||||
|
spliceField(dependency) |
||||||
|
} |
||||||
|
|
||||||
|
val clinit = clazz.methods.find { it.name == "<clinit>" } ?: createClinitMethod(clazz) |
||||||
|
// TODO(gpe): check the <clinit> method only has a single RETURN and that it is the last instruction |
||||||
|
clinit.maxStack = max(clinit.maxStack, field.initializer.maxStack) |
||||||
|
clinit.instructions.insertBefore(clinit.instructions.last, field.initializer.instructions) |
||||||
|
} |
||||||
|
|
||||||
|
clazz.version = ClassVersionUtils.max(clazz.version, field.version) |
||||||
|
clazz.fields.add(field.node) |
||||||
|
} |
||||||
|
|
||||||
|
private fun spliceMethods() { |
||||||
|
for (method in methods) { |
||||||
|
val clazz = classes.computeIfAbsent(method.owner, ::createClass) |
||||||
|
clazz.version = ClassVersionUtils.max(clazz.version, method.version) |
||||||
|
clazz.methods.add(method.node) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun removeEmptyClinitMethods() { |
||||||
|
for (clazz in classes.values) { |
||||||
|
val clinit = clazz.methods.find { it.name == "<clinit>" } ?: continue |
||||||
|
|
||||||
|
val first = clinit.instructions.firstOrNull { it.opcode != -1 } |
||||||
|
if (first != null && first.opcode == Opcodes.RETURN) { |
||||||
|
clazz.methods.remove(clinit) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun createClass(name: String): ClassNode { |
||||||
|
val clazz = ClassNode() |
||||||
|
clazz.version = Opcodes.V1_1 |
||||||
|
clazz.access = Opcodes.ACC_PUBLIC or Opcodes.ACC_SUPER or Opcodes.ACC_FINAL |
||||||
|
clazz.name = name |
||||||
|
clazz.superName = "java/lang/Object" |
||||||
|
clazz.interfaces = mutableListOf() |
||||||
|
clazz.innerClasses = mutableListOf() |
||||||
|
clazz.fields = mutableListOf() |
||||||
|
clazz.methods = mutableListOf() |
||||||
|
return clazz |
||||||
|
} |
||||||
|
|
||||||
|
private fun createClinitMethod(clazz: ClassNode): MethodNode { |
||||||
|
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.methods.add(clinit) |
||||||
|
|
||||||
|
return clinit |
||||||
|
} |
||||||
|
} |
@ -1,276 +0,0 @@ |
|||||||
package dev.openrs2.deob.transform |
|
||||||
|
|
||||||
import com.github.michaelbull.logging.InlineLogger |
|
||||||
import dev.openrs2.asm.ClassVersionUtils |
|
||||||
import dev.openrs2.asm.MemberDesc |
|
||||||
import dev.openrs2.asm.MemberRef |
|
||||||
import dev.openrs2.asm.classpath.ClassPath |
|
||||||
import dev.openrs2.asm.classpath.Library |
|
||||||
import dev.openrs2.asm.getExpression |
|
||||||
import dev.openrs2.asm.isSequential |
|
||||||
import dev.openrs2.asm.transform.Transformer |
|
||||||
import dev.openrs2.deob.Profile |
|
||||||
import dev.openrs2.util.collect.DisjointSet |
|
||||||
import org.objectweb.asm.Opcodes |
|
||||||
import org.objectweb.asm.tree.AbstractInsnNode |
|
||||||
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.MethodInsnNode |
|
||||||
import org.objectweb.asm.tree.MethodNode |
|
||||||
import javax.inject.Inject |
|
||||||
import javax.inject.Singleton |
|
||||||
import kotlin.math.max |
|
||||||
|
|
||||||
@Singleton |
|
||||||
class StaticScramblingTransformer @Inject constructor(private val profile: Profile) : Transformer() { |
|
||||||
private data class Field(val node: FieldNode, val initializer: InsnList, val version: Int, val maxStack: Int) { |
|
||||||
val dependencies = initializer.asSequence() |
|
||||||
.filterIsInstance<FieldInsnNode>() |
|
||||||
.filter { it.opcode == Opcodes.GETSTATIC } |
|
||||||
.map(::MemberRef) |
|
||||||
.toSet() |
|
||||||
} |
|
||||||
|
|
||||||
private lateinit var inheritedFieldSets: DisjointSet<MemberRef> |
|
||||||
private lateinit var inheritedMethodSets: DisjointSet<MemberRef> |
|
||||||
private val fields = mutableMapOf<DisjointSet.Partition<MemberRef>, Field>() |
|
||||||
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 MethodNode.extractEntryExitBlocks(): List<AbstractInsnNode> { |
|
||||||
/* |
|
||||||
* Most (or all?) of the <clinit> 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<Map<MemberDesc, InsnList>, Set<MemberDesc>> { |
|
||||||
val entryExitBlocks = extractEntryExitBlocks() |
|
||||||
|
|
||||||
val simpleInitializers = mutableMapOf<MemberDesc, InsnList>() |
|
||||||
val complexInitializers = instructions.asSequence() |
|
||||||
.filter { !entryExitBlocks.contains(it) } |
|
||||||
.filterIsInstance<FieldInsnNode>() |
|
||||||
.filter { it.opcode == Opcodes.GETSTATIC && it.owner == owner } |
|
||||||
.filter { !profile.excludedFields.matches(it.owner, it.name, it.desc) } |
|
||||||
.map(::MemberDesc) |
|
||||||
.toSet() |
|
||||||
|
|
||||||
val putstatics = entryExitBlocks |
|
||||||
.filterIsInstance<FieldInsnNode>() |
|
||||||
.filter { it.opcode == Opcodes.PUTSTATIC && it.owner == owner } |
|
||||||
.filter { !profile.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 |
|
||||||
|
|
||||||
val initializer = InsnList() |
|
||||||
for (insn in expr) { |
|
||||||
instructions.remove(insn) |
|
||||||
initializer.add(insn) |
|
||||||
} |
|
||||||
instructions.remove(putstatic) |
|
||||||
initializer.add(putstatic) |
|
||||||
|
|
||||||
simpleInitializers[desc] = initializer |
|
||||||
} |
|
||||||
|
|
||||||
return Pair(simpleInitializers, complexInitializers) |
|
||||||
} |
|
||||||
|
|
||||||
private fun spliceInitializers() { |
|
||||||
val done = mutableSetOf<DisjointSet.Partition<MemberRef>>() |
|
||||||
for ((partition, field) in fields) { |
|
||||||
spliceInitializers(done, partition, field) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private fun spliceInitializers( |
|
||||||
done: MutableSet<DisjointSet.Partition<MemberRef>>, |
|
||||||
partition: DisjointSet.Partition<MemberRef>, |
|
||||||
field: Field |
|
||||||
) { |
|
||||||
if (!done.add(partition)) { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
for (dependency in field.dependencies) { |
|
||||||
val dependencyPartition = inheritedFieldSets[dependency]!! |
|
||||||
val dependencyField = fields[dependencyPartition] ?: continue |
|
||||||
spliceInitializers(done, partition, dependencyField) |
|
||||||
} |
|
||||||
|
|
||||||
val (staticClass, clinit) = nextClass() |
|
||||||
staticClass.fields.add(field.node) |
|
||||||
staticClass.version = ClassVersionUtils.max(staticClass.version, field.version) |
|
||||||
clinit.instructions.insertBefore(clinit.instructions.last, field.initializer) |
|
||||||
clinit.maxStack = max(clinit.maxStack, field.maxStack) |
|
||||||
|
|
||||||
fieldClasses[partition] = staticClass.name |
|
||||||
} |
|
||||||
|
|
||||||
override fun preTransform(classPath: ClassPath) { |
|
||||||
inheritedFieldSets = classPath.createInheritedFieldSets() |
|
||||||
inheritedMethodSets = classPath.createInheritedMethodSets() |
|
||||||
fields.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 |
|
||||||
|
|
||||||
val clinit = clazz.methods.find { it.name == "<clinit>" } |
|
||||||
val (simpleInitializers, complexInitializers) = clinit?.extractInitializers(clazz.name) |
|
||||||
?: Pair(emptyMap(), emptySet()) |
|
||||||
|
|
||||||
clazz.fields.removeIf { field -> |
|
||||||
if (field.access and Opcodes.ACC_STATIC == 0) { |
|
||||||
return@removeIf false |
|
||||||
} else if (profile.excludedFields.matches(clazz.name, field.name, field.desc)) { |
|
||||||
return@removeIf false |
|
||||||
} |
|
||||||
|
|
||||||
val desc = MemberDesc(field) |
|
||||||
if (complexInitializers.contains(desc)) { |
|
||||||
return@removeIf false |
|
||||||
} |
|
||||||
|
|
||||||
val initializer = simpleInitializers[desc] ?: InsnList() |
|
||||||
val maxStack = clinit?.maxStack ?: 0 |
|
||||||
|
|
||||||
val partition = inheritedFieldSets[MemberRef(clazz, field)]!! |
|
||||||
fields[partition] = Field(field, initializer, clazz.version, maxStack) |
|
||||||
return@removeIf true |
|
||||||
} |
|
||||||
|
|
||||||
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 |
|
||||||
} 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 |
|
||||||
} |
|
||||||
|
|
||||||
val first = clinit?.instructions?.firstOrNull { it.opcode != -1 } |
|
||||||
if (first != null && first.opcode == Opcodes.RETURN) { |
|
||||||
clazz.methods.remove(clinit) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
spliceInitializers() |
|
||||||
|
|
||||||
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 |
|
||||||
} |
|
||||||
} |
|
Loading…
Reference in new issue