diff --git a/asm/src/main/java/dev/openrs2/asm/filter/Glob.kt b/asm/src/main/java/dev/openrs2/asm/filter/Glob.kt index 30013474..3d5f9fc4 100644 --- a/asm/src/main/java/dev/openrs2/asm/filter/Glob.kt +++ b/asm/src/main/java/dev/openrs2/asm/filter/Glob.kt @@ -22,7 +22,11 @@ object Glob { continue } - regex.append("[^/]*") + /* + * The deobfuscator uses ! in class names to separate the + * library name from the rest of the package/class name. + */ + regex.append("[^/!]*") } when (ch) { diff --git a/decompiler/src/main/java/dev/openrs2/decompiler/DecompileCommand.kt b/decompiler/src/main/java/dev/openrs2/decompiler/DecompileCommand.kt index 40f6b0a7..f293dfda 100644 --- a/decompiler/src/main/java/dev/openrs2/decompiler/DecompileCommand.kt +++ b/decompiler/src/main/java/dev/openrs2/decompiler/DecompileCommand.kt @@ -9,29 +9,47 @@ fun main(args: Array) = DecompileCommand().main(args) class DecompileCommand : CliktCommand(name = "decompile") { override fun run() { val deobOutput = Paths.get("nonfree/var/cache/deob") - val sources = listOf( - deobOutput.resolve("runescape_gl.jar"), - deobOutput.resolve("jaggl.jar"), - deobOutput.resolve("loader_gl.jar"), - deobOutput.resolve("signlink_gl.jar"), - deobOutput.resolve("unpack_gl.jar"), - deobOutput.resolve("unpackclass_gl.jar") + + val client = deobOutput.resolve("runescape_gl.jar") + val gl = deobOutput.resolve("jaggl.jar") + val loader = deobOutput.resolve("loader_gl.jar") + val signlink = deobOutput.resolve("signlink_gl.jar") + val unpack = deobOutput.resolve("unpack_gl.jar") + val unpackClass = deobOutput.resolve("unpackclass_gl.jar") + + val decompiler = Decompiler( + Library( + source = client, + destination = getDestination("client"), + dependencies = listOf(gl, signlink) + ), + Library( + source = gl, + destination = getDestination("gl") + ), + Library( + source = loader, + destination = getDestination("loader"), + dependencies = listOf(signlink, unpack) + ), + Library( + source = signlink, + destination = getDestination("signlink") + ), + Library( + source = unpack, + destination = getDestination("unpack") + ), + Library( + source = unpackClass, + destination = getDestination("unpackclass"), + dependencies = listOf(unpack) + ) ) - Decompiler(sources, ::getDestination).use { - it.run() - } + decompiler.run() } - private fun getDestination(archive: String): Path { - var dir = archive.replace(JAR_SUFFIX_REGEX, "") - when (dir) { - "runescape" -> dir = "client" - "jaggl" -> dir = "gl" - } + private fun getDestination(dir: String): Path { return Paths.get("nonfree").resolve(dir).resolve("src/main/java") } - - private companion object { - private val JAR_SUFFIX_REGEX = Regex("(?:_gl)?[.]jar$") - } } diff --git a/decompiler/src/main/java/dev/openrs2/decompiler/Decompiler.kt b/decompiler/src/main/java/dev/openrs2/decompiler/Decompiler.kt index 8e8ac430..8d1932f9 100644 --- a/decompiler/src/main/java/dev/openrs2/decompiler/Decompiler.kt +++ b/decompiler/src/main/java/dev/openrs2/decompiler/Decompiler.kt @@ -2,25 +2,21 @@ package dev.openrs2.decompiler import org.jetbrains.java.decompiler.main.Fernflower import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences -import java.io.Closeable -import java.nio.file.Path - -class Decompiler( - private val sources: List, - destination: (String) -> Path -) : Closeable { - private val io = DecompilerIo(destination) - private val fernflower = Fernflower(io, io, OPTIONS, Slf4jFernflowerLogger) +class Decompiler(private vararg val libraries: Library) { fun run() { - for (source in sources) { - fernflower.addSource(source.toFile()) - } - fernflower.decompileContext() - } + for (library in libraries) { + DecompilerIo(library.destination).use { io -> + val fernflower = Fernflower(io, io, OPTIONS, Slf4jFernflowerLogger) - override fun close() { - io.close() + for (dependency in library.dependencies) { + fernflower.addLibrary(dependency.toFile()) + } + fernflower.addSource(library.source.toFile()) + + fernflower.decompileContext() + } + } } private companion object { diff --git a/decompiler/src/main/java/dev/openrs2/decompiler/DecompilerIo.kt b/decompiler/src/main/java/dev/openrs2/decompiler/DecompilerIo.kt index eb028b15..657c842f 100644 --- a/decompiler/src/main/java/dev/openrs2/decompiler/DecompilerIo.kt +++ b/decompiler/src/main/java/dev/openrs2/decompiler/DecompilerIo.kt @@ -8,7 +8,7 @@ import java.nio.file.Path import java.util.jar.JarFile import java.util.jar.Manifest -class DecompilerIo(private val destination: (String) -> Path) : IBytecodeProvider, IResultSaver, Closeable { +class DecompilerIo(private val destination: Path) : IBytecodeProvider, IResultSaver, Closeable { private val inputJars = mutableMapOf() override fun getBytecode(externalPath: String, internalPath: String?): ByteArray { @@ -62,7 +62,7 @@ class DecompilerIo(private val destination: (String) -> Path) : IBytecodeProvide entryName: String, content: String ) { - val p = destination(archiveName).resolve(entryName) + val p = destination.resolve(entryName) Files.createDirectories(p.parent) Files.newBufferedWriter(p).use { it.write(content) diff --git a/decompiler/src/main/java/dev/openrs2/decompiler/Library.kt b/decompiler/src/main/java/dev/openrs2/decompiler/Library.kt new file mode 100644 index 00000000..49615764 --- /dev/null +++ b/decompiler/src/main/java/dev/openrs2/decompiler/Library.kt @@ -0,0 +1,9 @@ +package dev.openrs2.decompiler + +import java.nio.file.Path + +class Library( + val source: Path, + val destination: Path, + val dependencies: List = emptyList() +) diff --git a/deob/src/main/java/dev/openrs2/deob/Deobfuscator.kt b/deob/src/main/java/dev/openrs2/deob/Deobfuscator.kt index f00f9a74..4be4fa6c 100644 --- a/deob/src/main/java/dev/openrs2/deob/Deobfuscator.kt +++ b/deob/src/main/java/dev/openrs2/deob/Deobfuscator.kt @@ -7,7 +7,8 @@ import dev.openrs2.asm.io.JarLibraryReader import dev.openrs2.asm.io.JarLibraryWriter import dev.openrs2.asm.io.Pack200LibraryReader import dev.openrs2.asm.transform.Transformer -import dev.openrs2.deob.remap.PrefixRemapper +import dev.openrs2.deob.remap.ClassNamePrefixRemapper +import dev.openrs2.deob.remap.StripClassNamePrefixRemapper import java.nio.file.Files import java.nio.file.Path import javax.inject.Inject @@ -15,7 +16,6 @@ import javax.inject.Singleton @Singleton class Deobfuscator @Inject constructor( - private val profile: Profile, @DeobfuscatorQualifier private val transformers: Set<@JvmSuppressWildcards Transformer> ) { fun run(input: Path, output: Path) { @@ -36,10 +36,47 @@ class Deobfuscator @Inject constructor( val unpack = Library("unpack") unpack.add(loader.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(PrefixRemapper.create(loader, "loader_", profile.excludedClasses)) - unpackClass.remap(PrefixRemapper.create(unpackClass, "unpackclass_", profile.excludedClasses)) + /* + * Prefix class names with the name of the library the class + * came from (e.g. `a` => `client!a`). + * + * Using ! as the separator was chosen because it is not valid in Java + * source code, so we won't expect to see it in the obfuscator's input. + * Furthermore, if any prefixes accidentally remain unstripped, the + * problem will be detected quickly as the deobfuscator's output will + * not compile. It also mirrors the syntax used in JarURLConnection, + * which has a similar purpose. + * + * In the early parts of the deobfuscation pipeline, this allows us to + * disambiguate a small number of classes in the signlink which clash + * with classes in the client. + * + * After name mapping has been performed, it allows us to disambiguate + * classes across separate libraries that have been refactored and + * given the same name. + * + * For example, the client and unpackclass both contain many common + * classes (e.g. the exception wrapper, linked list/node classes, + * bzip2/gzip decompression classes, and so on). Giving these the same + * names across both the client and unpackclass is desirable. + * + * (Unfortunately we can't deduplicate the classes, as they both expose + * different sets of fields/methods, presumably as a result of the + * obfuscator removing unused code.) + */ + val clientRemapper = ClassNamePrefixRemapper(client, gl, signlink) + val glRemapper = ClassNamePrefixRemapper(gl) + val loaderRemapper = ClassNamePrefixRemapper(loader, signlink, unpack) + val signlinkRemapper = ClassNamePrefixRemapper(signlink) + val unpackClassRemapper = ClassNamePrefixRemapper(unpackClass, unpack) + val unpackRemapper = ClassNamePrefixRemapper(unpack) + + client.remap(clientRemapper) + gl.remap(glRemapper) + loader.remap(loaderRemapper) + signlink.remap(signlinkRemapper) + unpack.remap(unpackRemapper) + unpackClass.remap(unpackClassRemapper) // bundle libraries together into a common classpath val runtime = ClassLoader.getPlatformClassLoader() @@ -56,6 +93,14 @@ class Deobfuscator @Inject constructor( transformer.transform(classPath) } + // strip class name prefixes + client.remap(StripClassNamePrefixRemapper) + gl.remap(StripClassNamePrefixRemapper) + loader.remap(StripClassNamePrefixRemapper) + signlink.remap(StripClassNamePrefixRemapper) + unpack.remap(StripClassNamePrefixRemapper) + unpackClass.remap(StripClassNamePrefixRemapper) + // write output jars logger.info { "Writing output jars" } diff --git a/deob/src/main/java/dev/openrs2/deob/SignedClassUtils.kt b/deob/src/main/java/dev/openrs2/deob/SignedClassUtils.kt index a38eeb93..c1f1f8e4 100644 --- a/deob/src/main/java/dev/openrs2/deob/SignedClassUtils.kt +++ b/deob/src/main/java/dev/openrs2/deob/SignedClassUtils.kt @@ -4,9 +4,6 @@ import com.github.michaelbull.logging.InlineLogger import dev.openrs2.asm.InsnMatcher import dev.openrs2.asm.classpath.Library import org.objectweb.asm.Type -import org.objectweb.asm.commons.ClassRemapper -import org.objectweb.asm.commons.SimpleRemapper -import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.LdcInsnNode import org.objectweb.asm.tree.MethodNode @@ -23,13 +20,6 @@ object SignedClassUtils { val dependencies = findDependencies(loader, signedClasses) logger.info { "Identified signed class dependencies $dependencies" } - // rename dependencies of signed classes so they don't clash with client classes - val mapping = mutableMapOf() - for (dependency in dependencies) { - mapping[dependency] = "loader_$dependency" - } - val remapper = SimpleRemapper(mapping) - // delete original signed classes (these have no dependencies) for (name in signedClasses) { client.remove(name) @@ -37,12 +27,7 @@ object SignedClassUtils { // move loader signed classes to signlink for (name in signedClasses union dependencies) { - val `in` = loader.remove(name)!! - - val out = ClassNode() - `in`.accept(ClassRemapper(out, remapper)) - - signLink.add(out) + signLink.add(loader.remove(name)!!) } } diff --git a/deob/src/main/java/dev/openrs2/deob/remap/ClassMappingGenerator.kt b/deob/src/main/java/dev/openrs2/deob/remap/ClassMappingGenerator.kt index b3af008d..e4db83b3 100644 --- a/deob/src/main/java/dev/openrs2/deob/remap/ClassMappingGenerator.kt +++ b/deob/src/main/java/dev/openrs2/deob/remap/ClassMappingGenerator.kt @@ -19,7 +19,11 @@ class ClassMappingGenerator( populateMapping(clazz) } - mapping.replaceAll(nameMap::mapClassName) + mapping.replaceAll { k, v -> + val (library, default) = v.splitAtLibraryBoundary() + val name = nameMap.mapClassName(k, default) + return@replaceAll "$library!$name" + } return mapping } @@ -51,12 +55,12 @@ class ClassMappingGenerator( private fun generateName(clazz: ClassMetadata): String { val name = clazz.name - var mappedName = name.substring(0, name.lastIndexOf('/') + 1) + var mappedName = name.getLibraryAndPackageName() val superClass = clazz.superClass if (superClass != null && superClass.name != "java/lang/Object") { var superName = populateMapping(superClass) - superName = superName.substring(superName.lastIndexOf('/') + 1) + superName = superName.getClassName() mappedName += nameGenerator.generate(superName + "_Sub") } else if (clazz.`interface`) { mappedName += nameGenerator.generate("Interface") diff --git a/deob/src/main/java/dev/openrs2/deob/remap/ClassNamePrefix.kt b/deob/src/main/java/dev/openrs2/deob/remap/ClassNamePrefix.kt new file mode 100644 index 00000000..0cd19876 --- /dev/null +++ b/deob/src/main/java/dev/openrs2/deob/remap/ClassNamePrefix.kt @@ -0,0 +1,44 @@ +package dev.openrs2.deob.remap + +import dev.openrs2.asm.classpath.ExtendedRemapper +import dev.openrs2.asm.classpath.Library + +private val BOUNDARY_CHARS = charArrayOf('/', '!') + +fun String.splitAtLibraryBoundary(): Pair { + val index = indexOf('!') + return Pair(substring(0, index), substring(index + 1)) +} + +fun String.getLibraryAndPackageName(): String { + return substring(0, lastIndexOfAny(BOUNDARY_CHARS) + 1) +} + +fun String.getClassName(): String { + return substring(lastIndexOfAny(BOUNDARY_CHARS) + 1) +} + +class ClassNamePrefixRemapper(vararg libraries: Library) : ExtendedRemapper() { + private val mapping = mutableMapOf() + + init { + for (library in libraries) { + for (clazz in library) { + require(!clazz.name.contains('!')) { + "Input class name contains !, which conflicts with library separator" + } + mapping.putIfAbsent(clazz.name, "${library.name}!${clazz.name}") + } + } + } + + override fun map(internalName: String): String { + return mapping.getOrDefault(internalName, internalName) + } +} + +object StripClassNamePrefixRemapper : ExtendedRemapper() { + override fun map(internalName: String): String { + return internalName.substring(internalName.indexOf('!') + 1) + } +} diff --git a/deob/src/main/java/dev/openrs2/deob/remap/FieldMappingGenerator.kt b/deob/src/main/java/dev/openrs2/deob/remap/FieldMappingGenerator.kt index 2f7a5c98..7e46b4a8 100644 --- a/deob/src/main/java/dev/openrs2/deob/remap/FieldMappingGenerator.kt +++ b/deob/src/main/java/dev/openrs2/deob/remap/FieldMappingGenerator.kt @@ -61,7 +61,7 @@ class FieldMappingGenerator( } Type.OBJECT -> { val className = classMapping.getOrDefault(elementType.internalName, elementType.internalName) - className.substring(className.lastIndexOf('/') + 1) + dimensions + className.getClassName() + dimensions } else -> throw IllegalArgumentException("Unknown field type $elementType") } diff --git a/deob/src/main/java/dev/openrs2/deob/remap/PrefixRemapper.kt b/deob/src/main/java/dev/openrs2/deob/remap/PrefixRemapper.kt deleted file mode 100644 index b270225a..00000000 --- a/deob/src/main/java/dev/openrs2/deob/remap/PrefixRemapper.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.openrs2.deob.remap - -import dev.openrs2.asm.classpath.ExtendedRemapper -import dev.openrs2.asm.classpath.Library -import dev.openrs2.asm.filter.ClassFilter - -class PrefixRemapper(private val prefix: String, private val classes: Set) : ExtendedRemapper() { - override fun map(internalName: String): String { - return if (classes.contains(internalName)) { - prefix + internalName - } else { - internalName - } - } - - companion object { - fun create(library: Library, prefix: String, excluded: ClassFilter): ExtendedRemapper { - val classes = mutableSetOf() - - for (clazz in library) { - if (!excluded.matches(clazz.name)) { - classes += clazz.name - } - } - - return PrefixRemapper(prefix, classes) - } - } -} diff --git a/deob/src/main/java/dev/openrs2/deob/remap/StaticFieldUnscrambler.kt b/deob/src/main/java/dev/openrs2/deob/remap/StaticFieldUnscrambler.kt index eaf47fa8..4cc2e76c 100644 --- a/deob/src/main/java/dev/openrs2/deob/remap/StaticFieldUnscrambler.kt +++ b/deob/src/main/java/dev/openrs2/deob/remap/StaticFieldUnscrambler.kt @@ -51,7 +51,7 @@ class StaticFieldUnscrambler( val member = MemberRef(clazz, field) val partition = inheritedFieldSets[member]!! val owner = nameMap.mapFieldOwner(partition, generator.generate()) - fields[partition] = StaticField(owner, simpleInitializers[desc]) + fields[partition] = StaticField("${library.name}!$owner", simpleInitializers[desc]) } } } diff --git a/deob/src/main/java/dev/openrs2/deob/remap/StaticMethodUnscrambler.kt b/deob/src/main/java/dev/openrs2/deob/remap/StaticMethodUnscrambler.kt index c2af9c40..234de3e3 100644 --- a/deob/src/main/java/dev/openrs2/deob/remap/StaticMethodUnscrambler.kt +++ b/deob/src/main/java/dev/openrs2/deob/remap/StaticMethodUnscrambler.kt @@ -37,7 +37,8 @@ class StaticMethodUnscrambler( val member = MemberRef(clazz, method) val partition = inheritedMethodSets[member]!! - owners[partition] = nameMap.mapMethodOwner(partition, generator.generate()) + val owner = nameMap.mapMethodOwner(partition, generator.generate()) + owners[partition] = "${library.name}!$owner" } } } 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 479b2573..81cd9bf0 100644 --- a/deob/src/main/java/dev/openrs2/deob/remap/TypedRemapper.kt +++ b/deob/src/main/java/dev/openrs2/deob/remap/TypedRemapper.kt @@ -57,8 +57,6 @@ class TypedRemapper private constructor( companion object { private val logger = InlineLogger() - private val LIBRARY_PREFIX_REGEX = Regex("^(?:loader|unpackclass)_") - fun create(classPath: ClassPath, profile: Profile, nameMap: NameMap): TypedRemapper { val inheritedFieldSets = classPath.createInheritedFieldSets() val inheritedMethodSets = classPath.createInheritedMethodSets() @@ -133,7 +131,7 @@ class TypedRemapper private constructor( } private fun verifyMapping(name: String, mappedName: String, maxObfuscatedNameLen: Int) { - val originalName = name.replace(LIBRARY_PREFIX_REGEX, "") + val originalName = StripClassNamePrefixRemapper.map(name) if (originalName.length > maxObfuscatedNameLen && originalName != mappedName) { logger.warn { "Remapping probably unobfuscated name $originalName to $mappedName" } } diff --git a/deob/src/main/java/dev/openrs2/deob/transform/ResetTransformer.kt b/deob/src/main/java/dev/openrs2/deob/transform/ResetTransformer.kt index 6b4e43e4..3e0459c1 100644 --- a/deob/src/main/java/dev/openrs2/deob/transform/ResetTransformer.kt +++ b/deob/src/main/java/dev/openrs2/deob/transform/ResetTransformer.kt @@ -35,7 +35,7 @@ class ResetTransformer : Transformer() { val masterReset = findMasterReset(method) ?: continue logger.info { "Identified master reset method $masterReset" } - val resetClass = classPath.getClassNode("client")!! + val resetClass = classPath.getClassNode("client!client")!! val resetMethod = resetClass.methods.first { it.name == masterReset.name && it.desc == masterReset.desc } diff --git a/share/deob/profile.yaml b/share/deob/profile.yaml index 9304431e..95ee4eb5 100644 --- a/share/deob/profile.yaml +++ b/share/deob/profile.yaml @@ -1,36 +1,36 @@ --- excluded_classes: - - "client" - - "com/sun/opengl/impl/x11/**" - - "jagex3/jagmisc/jagmisc" - - "jaggl/**" - - "javax/media/opengl/**" - - "loader" - - "unpack" - - "unpackclass" + - "*!client" + - "*!com/sun/opengl/impl/x11/**" + - "*!jagex3/jagmisc/jagmisc" + - "*!jaggl/**" + - "*!javax/media/opengl/**" + - "*!loader" + - "*!unpack" + - "*!unpackclass" excluded_methods: - - "**. *" - - "**. *" - - "**.main *" - - "**.providesignlink *" - - "**.quit *" - - "com/sun/opengl/impl/x11/**.* *" - - "jaggl/**.* *" - - "javax/media/opengl/**.* *" + - "*!**. *" + - "*!**. *" + - "*!**.main *" + - "*!**.providesignlink *" + - "*!**.quit *" + - "*!com/sun/opengl/impl/x11/**.* *" + - "*!jaggl/**.* *" + - "*!javax/media/opengl/**.* *" excluded_fields: - - "**.cache *" - - "com/sun/opengl/impl/x11/**.* *" - - "jaggl/**.* *" - - "javax/media/opengl/**.* *" + - "*!**.cache *" + - "*!com/sun/opengl/impl/x11/**.* *" + - "*!jaggl/**.* *" + - "*!javax/media/opengl/**.* *" entry_points: - - "**. *" - - "**.main *" - - "**.providesignlink *" - - "client. *" - - "com/sun/opengl/impl/x11/DRIHack.begin *" - - "com/sun/opengl/impl/x11/DRIHack.end *" - - "loader. *" - - "unpackclass. *" + - "*!**. *" + - "*!**.main *" + - "*!**.providesignlink *" + - "*!client. *" + - "*!com/sun/opengl/impl/x11/DRIHack.begin *" + - "*!com/sun/opengl/impl/x11/DRIHack.end *" + - "*!loader. *" + - "*!unpackclass. *" scrambled_libraries: - client max_obfuscated_name_len: 2