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/Deobfuscator.kt

205 lines
8.4 KiB

package dev.openrs2.deob
import com.github.michaelbull.logging.InlineLogger
import dev.openrs2.asm.classpath.ClassPath
import dev.openrs2.asm.classpath.Library
import dev.openrs2.asm.io.JarLibraryReader
import dev.openrs2.asm.io.JarLibraryWriter
import dev.openrs2.asm.io.Pack200LibraryReader
import dev.openrs2.bundler.Bundler
import dev.openrs2.bundler.transform.ResourceTransformer
import dev.openrs2.deob.SignedClassUtils.move
import dev.openrs2.deob.remap.PrefixRemapper.create
import dev.openrs2.deob.transform.BitShiftTransformer
import dev.openrs2.deob.transform.BitwiseOpTransformer
import dev.openrs2.deob.transform.CanvasTransformer
import dev.openrs2.deob.transform.ClassLiteralTransformer
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
import dev.openrs2.deob.transform.ConstantArgTransformer
import dev.openrs2.deob.transform.CounterTransformer
import dev.openrs2.deob.transform.EmptyClassTransformer
import dev.openrs2.deob.transform.ExceptionTracingTransformer
import dev.openrs2.deob.transform.FernflowerExceptionTransformer
import dev.openrs2.deob.transform.FieldOrderTransformer
import dev.openrs2.deob.transform.FinalTransformer
import dev.openrs2.deob.transform.InvokeSpecialTransformer
import dev.openrs2.deob.transform.MethodOrderTransformer
import dev.openrs2.deob.transform.MonitorTransformer
import dev.openrs2.deob.transform.OpaquePredicateTransformer
import dev.openrs2.deob.transform.OriginalNameTransformer
import dev.openrs2.deob.transform.OriginalPcRestoreTransformer
import dev.openrs2.deob.transform.OriginalPcSaveTransformer
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
import dev.openrs2.deob.transform.VisibilityTransformer
import dev.openrs2.util.io.DeterministicJarOutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.util.jar.JarInputStream
class Deobfuscator(private val input: Path, private val output: Path) {
fun run() {
// read input jars/packs
logger.info { "Reading input jars" }
val unpackClass = readJar(input.resolve("unpackclass.pack"))
val glUnpackClass = Library(unpackClass)
val loader = readJar(input.resolve("loader.jar"))
val glLoader = readJar(input.resolve("loader_gl.jar"))
val gl = readPack(input.resolve("jaggl.pack200"))
val client = readJar(input.resolve("runescape.jar"))
val glClient = readPack(input.resolve("runescape_gl.pack200"))
// TODO(gpe): it'd be nice to have separate signlink.jar and
// signlink-unsigned.jar files so we don't (effectively) deobfuscate
// runescape.jar twice with different sets of names, but thinking about
// how this would work is tricky (as the naming must match)
val unsignedClient = Library(client)
// overwrite client's classes with signed classes from the loader
logger.info { "Moving signed classes from loader" }
val signLink = Library()
move(loader, client, signLink)
logger.info { "Moving signed classes from loader_gl" }
val glSignLink = Library()
move(glLoader, glClient, glSignLink)
// move unpack class out of the loader (so the unpacker and loader can both depend on it)
logger.info { "Moving unpack from loader to unpack" }
val unpack = Library()
unpack.add(loader.remove("unpack")!!)
logger.info { "Moving unpack from loader_gl to unpack_gl" }
val glUnpack = Library()
glUnpack.add(glLoader.remove("unpack")!!)
// prefix remaining loader/unpacker classes (to avoid conflicts when we rename in the same classpath as the client)
logger.info { "Prefixing loader and unpackclass class names" }
loader.remap(create(loader, "loader_"))
glLoader.remap(create(glLoader, "loader_"))
unpackClass.remap(create(unpackClass, "unpackclass_"))
glUnpackClass.remap(create(glUnpackClass, "unpackclass_"))
// bundle libraries together into a common classpath
val runtime = ClassLoader.getPlatformClassLoader()
val classPath = ClassPath(
runtime,
dependencies = emptyList(),
libraries = listOf(client, loader, signLink, unpack, unpackClass)
)
val glClassPath = ClassPath(
runtime,
dependencies = listOf(gl),
libraries = listOf(glClient, glLoader, glSignLink, glUnpack, glUnpackClass)
)
val unsignedClassPath = ClassPath(
runtime,
dependencies = emptyList(),
libraries = listOf(unsignedClient)
)
// deobfuscate
logger.info { "Transforming client" }
for (transformer in TRANSFORMERS) {
logger.info { "Running transformer ${transformer.javaClass.simpleName} " }
transformer.transform(classPath)
}
logger.info { "Transforming client_gl" }
for (transformer in TRANSFORMERS) {
logger.info { "Running transformer ${transformer.javaClass.simpleName} " }
transformer.transform(glClassPath)
}
logger.info { "Transforming client_unsigned" }
for (transformer in TRANSFORMERS) {
logger.info { "Running transformer ${transformer.javaClass.simpleName} " }
transformer.transform(unsignedClassPath)
}
// write output jars
logger.info { "Writing output jars" }
Files.createDirectories(output)
writeJar(classPath, client, output.resolve("runescape.jar"))
writeJar(classPath, loader, output.resolve("loader.jar"))
writeJar(classPath, signLink, output.resolve("signlink.jar"))
writeJar(classPath, unpack, output.resolve("unpack.jar"))
writeJar(classPath, unpackClass, output.resolve("unpackclass.jar"))
writeJar(glClassPath, gl, output.resolve("jaggl.jar"))
writeJar(glClassPath, glClient, output.resolve("runescape_gl.jar"))
writeJar(glClassPath, glLoader, output.resolve("loader_gl.jar"))
writeJar(glClassPath, glSignLink, output.resolve("signlink_gl.jar"))
writeJar(glClassPath, glUnpack, output.resolve("unpack_gl.jar"))
writeJar(glClassPath, glUnpackClass, output.resolve("unpackclass_gl.jar"))
writeJar(unsignedClassPath, unsignedClient, output.resolve("runescape_unsigned.jar"))
}
private fun readJar(path: Path): Library {
logger.info { "Reading jar $path" }
return JarInputStream(Files.newInputStream(path)).use { input ->
JarLibraryReader(input).read()
}
}
private fun readPack(path: Path): Library {
logger.info { "Reading pack $path" }
return Files.newInputStream(path).use { input ->
Pack200LibraryReader(input).read()
}
}
private fun writeJar(classPath: ClassPath, library: Library, path: Path) {
logger.info { "Writing jar $path" }
DeterministicJarOutputStream(Files.newOutputStream(path)).use { output ->
JarLibraryWriter(output).write(classPath, library)
}
}
companion object {
private val logger = InlineLogger()
private val TRANSFORMERS = listOf(
OriginalPcSaveTransformer(),
OriginalNameTransformer(),
*Bundler.TRANSFORMERS.toTypedArray(),
ResourceTransformer(),
OpaquePredicateTransformer(),
ExceptionTracingTransformer(),
MonitorTransformer(),
BitShiftTransformer(),
CanvasTransformer(),
FieldOrderTransformer(),
BitwiseOpTransformer(),
RemapTransformer(),
Replace DummyArgTransformer with new ConstantArgTransformer The new transformer uses a different approach to the old one. It starts exploring the call graph from the entry points, recursively analysing method calls. Methods are only re-analysed if their possible argument values change, with the Unknown value being used if we can't identify a single integer constant at a call site. This prevents us from recursing infinitely if the client code does. While this first pass does simplify branches in order to ignore dummy method calls that are never evaluated at runtime, it operates on a copy of the method (as we initially ignore more calls while the argument value sets are smaller, ignoring fewer calls as they build up). A separate second pass simplifies branches on the original method and inlines singleton constants, paving the way for the UnusedArgTransformer to actually remove the newly unused arguments. This new approach has several benefits: - It is much faster than the old approach, as we only re-analyse methods as required by argument value changes, rather than re-analysing every method during every pass. - It doesn't require special cases for dealing with mutually recursive dummy calls. The old approach hard-coded special cases for mutually recursive calls involving groups of 1 and 2 methods. The code for this wasn't clean. Furthermore, while it was just about good enough for the HD client, the SD client contains a mutually recursive group of 3 methods. The new approach is capable of dealing with mutually recursive groups of any size. Finally, the new transformer has a much cleaner implementation. Signed-off-by: Graham <gpe@openrs2.dev>
4 years ago
ConstantArgTransformer(),
UnusedLocalTransformer(),
UnusedMethodTransformer(),
UnusedArgTransformer(),
CounterTransformer(),
ResetTransformer(),
ClassLiteralTransformer(),
InvokeSpecialTransformer(),
StaticScramblingTransformer(),
EmptyClassTransformer(),
MethodOrderTransformer(),
VisibilityTransformer(),
FinalTransformer(),
OverrideTransformer(),
RedundantGotoTransformer(),
OriginalPcRestoreTransformer(),
FernflowerExceptionTransformer()
)
}
}