Combine the StaticScramblingTransformer with the RemapTransformer

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>
Graham 5 years ago
parent cccbbe4d19
commit 7d9b1c61e8
  1. 10
      asm/src/main/java/dev/openrs2/asm/classpath/Library.kt
  2. 219
      asm/src/main/java/dev/openrs2/asm/classpath/LibraryRemapper.kt
  3. 6
      deob/src/main/java/dev/openrs2/deob/DeobfuscatorModule.kt
  4. 12
      deob/src/main/java/dev/openrs2/deob/transform/InvokeSpecialTransformer.kt
  5. 276
      deob/src/main/java/dev/openrs2/deob/transform/StaticScramblingTransformer.kt

@ -3,14 +3,14 @@ package dev.openrs2.asm.classpath
import com.github.michaelbull.logging.InlineLogger
import dev.openrs2.asm.io.LibraryReader
import dev.openrs2.asm.io.LibraryWriter
import dev.openrs2.asm.remap
import org.objectweb.asm.tree.ClassNode
import java.nio.file.Files
import java.nio.file.Path
import java.util.SortedMap
import java.util.TreeMap
class Library() : Iterable<ClassNode> {
private var classes = TreeMap<String, ClassNode>()
private var classes: SortedMap<String, ClassNode> = TreeMap()
constructor(library: Library) : this() {
for (clazz in library.classes.values) {
@ -41,11 +41,7 @@ class Library() : Iterable<ClassNode> {
}
fun remap(remapper: ExtendedRemapper) {
for (clazz in classes.values) {
clazz.remap(remapper)
}
classes = classes.mapKeysTo(TreeMap()) { (_, clazz) -> clazz.name }
classes = LibraryRemapper(remapper, classes).remap()
}
fun write(path: Path, writer: LibraryWriter, classPath: ClassPath) {

@ -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
}
}

@ -30,7 +30,6 @@ import dev.openrs2.deob.transform.OverrideTransformer
import dev.openrs2.deob.transform.RedundantGotoTransformer
import dev.openrs2.deob.transform.RemapTransformer
import dev.openrs2.deob.transform.ResetTransformer
import dev.openrs2.deob.transform.StaticScramblingTransformer
import dev.openrs2.deob.transform.UnusedArgTransformer
import dev.openrs2.deob.transform.UnusedLocalTransformer
import dev.openrs2.deob.transform.UnusedMethodTransformer
@ -57,6 +56,8 @@ object DeobfuscatorModule : AbstractModule() {
binder.addBinding().to(CanvasTransformer::class.java)
binder.addBinding().to(FieldOrderTransformer::class.java)
binder.addBinding().to(BitwiseOpTransformer::class.java)
binder.addBinding().to(ClassLiteralTransformer::class.java)
binder.addBinding().to(InvokeSpecialTransformer::class.java)
binder.addBinding().to(RemapTransformer::class.java)
binder.addBinding().to(ConstantArgTransformer::class.java)
binder.addBinding().to(UnusedLocalTransformer::class.java)
@ -64,9 +65,6 @@ object DeobfuscatorModule : AbstractModule() {
binder.addBinding().to(UnusedArgTransformer::class.java)
binder.addBinding().to(CounterTransformer::class.java)
binder.addBinding().to(ResetTransformer::class.java)
binder.addBinding().to(ClassLiteralTransformer::class.java)
binder.addBinding().to(InvokeSpecialTransformer::class.java)
binder.addBinding().to(StaticScramblingTransformer::class.java)
binder.addBinding().to(EmptyClassTransformer::class.java)
binder.addBinding().to(MethodOrderTransformer::class.java)
binder.addBinding().to(VisibilityTransformer::class.java)

@ -33,10 +33,10 @@ import javax.inject.Singleton
* instruction, and non-virtual function calls are cheaper than virtual
* function calls) or an obfuscation technique.
*
* The [StaticScramblingTransformer] moves static methods containing these
* `INVOKESPECIAL` instructions to new classes. This change is still permitted
* by the JVM specification, which does not place any restrictions on the class
* referenced by the `INVOKESPECIAL` instruction.
* The [RemapTransformer] moves static methods containing these `INVOKESPECIAL`
* instructions to new classes. This change is still permitted by the JVM
* specification, which does not place any restrictions on the class referenced
* by the `INVOKESPECIAL` instruction.
*
* However, the
* [verifier](https://github.com/openjdk/jdk11u/blob/3789983e89c9de252ef546a1b98a732a7d066650/src/java.base/share/native/libverify/check_code.c#L1342)
@ -45,8 +45,8 @@ import javax.inject.Singleton
* direct superclass illegal.
*
* This transformer replaces `INVOKESPECIAL` with equivalent `INVOKEVIRTUAL`
* instructions where possible, allowing the [StaticScramblingTransformer] to
* produce verifiable output.
* instructions where possible, allowing the [RemapTransformer] to produce
* verifiable output.
*/
@Singleton
class InvokeSpecialTransformer : Transformer() {

@ -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…
Cancel
Save