Skip to content

Commit

Permalink
first code commit
Browse files Browse the repository at this point in the history
  • Loading branch information
octarine-noise committed Feb 27, 2016
1 parent 944f013 commit 61a0273
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 3 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
build/
.idea/
*.iml
.gradle/
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2016
Copyright (c) 2016 octarine-noise

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
110 changes: 108 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,108 @@
# simpledeobf
simple deobfuscator for Minecraft mods
### What is this?

**simpledeobf** is a very simple and primitive command-line deobfuscator for
Minecraft mods. You can use it to create dev versions of mod releases
for debugging. Or anything else, really - it all depends on what inputs and
mapping files you use. Feel free to get creative with it.

### How to use it?

simpledeobf will need files from your Gradle cache which are created by
ForgeGradle, so it's best to start with a ForgeGradle workspace already
set up for your target Minecraft version.

Command-line options:

* `--output` The path to the resulting jar. You can only have one.
* `--input` The input jar you wish to deobfuscate. Can be repeated, but the
files from all inputs will just end up in a single output jar.
* `--mapFile` The path to the MCP mapping file, found in your Gradle cache
somewhere under `minecraft/de/oceanlabs/mcp`. You need to use the one that
corresponds to the namespaces you are converting from and to, and also
matches the ones you use in your project. Can be repeated, later files will
overwrite mappings from previous ones.
* `--map` Defines a single explicit mapping. This option is treated as if
it was a line in the MCP mapping file. Takes precedence over mappings
read from MCP files. Can be repeated.
* `--ref` The path to a reference jar. Can be repeated. The classes inside
are read only to determine the class hierarchy, which may be needed to properly
deobfuscate certain jar files. The reference jar must be in the same namespace
as the input.

More precisely: you will need to provide a proper class hierarchy
to deobfuscate overridden methods whose declaring classes or interfaces are
2 or more levels above the overriding class.

When in doubt, try using a Minecraft jar of the proper namespace.
* `--defaultPkg` move all classes from the default package into this one. Having
classes in the default package will mess with source attachment in the IDE.
* `--forcePublic` make all fields, methods and inner classes public. The
poor man's access transformer.
* `--help` or `-?` displays a quick overview of these options.

Have your favourite brand of decompiler ready, and be prepared to dig through
the result, and make several iterations before you end up with something
usable.

Also there's no guarantee that it's even possible to create a workable dev jar
for any given mod. If it uses reflection and/or class transformation, chances
are good that it will just crash anyway, unless it's specifically made to be
environment agnostic.

### Have OptiFine, will debug

As an example, here is a guide on getting OptiFine into your dev
environment with simpledeobf. This is *the* prime use-case of simpledeobf,
and the reason for its existence.

The following is the actual command I use for the deobfuscation itself,
so you'll need to change the directory names. It's a single command, but I
broke it up into lines for readability.
```
java -jar simpledeobf-0.5.jar
--input h:\Minecraft\mods\obf\OptiFine_1.8.8_HD_U_H2.jar
--output h:\Minecraft\mods\mcp\OptiFine_1.8.8_HD_U_H2-dev.jar
--mapFile h:\Minecraft\.gradle\caches\minecraft\de\oceanlabs\mcp\mcp_stable\20\srgs\notch-mcp.srg
--ref h:\Minecraft\.gradle\caches\minecraft\net\minecraft\minecraft_merged\1.8.8\minecraft_merged-1.8.8.jar
--map="CL: bet$1 net/minecraft/client/entity/AbstractClientPlayer$1"
--map="CL: b$8 net/minecraft/crash/CrashReport$8"
--defaultPkg optifineroot
--forcePublic
```
This will give you a jar with the `stable_20` mappings. The 2 `map` options are
needed because OptiFine declares some extra inner classes that are not present
in vanilla and have no mappings. If you want to do this for a different Minecraft
version, you'll have to change these. Just start without manual mappings, and
check if there are still obfuscated classes in the result. Check in the MCP
files what the outer class is, and add a mapping. Rinse and repeat.

Open the resulting jar file, and delete the `net/minecraftforge` directory.
There are dummy classes inside that are normally not loaded, but will cause
problems in a dev environment. They need to go.

Now you have to create a tweaker and class transformer, because the default
OptiFine ones will not work properly in a dev environment.
[Here is mine](https://github.com/octarine-noise/BetterFoliage/blob/c0be72bb37311508c68db5bd3b09d2f99a76614c/src/main/kotlin/optifine/OptifineTweakerDevWrapper.kt)
that I use in Better Foliage, you need something similar. The point is to
change dots to slashes in the class names, so the OptiFine transformer can find
the deobfuscated class files.

Edit the `META-INF/MANIFEST.MF` file. Change the `TweakClass` option to the
tweaker you just made.

The jar is now ready. If you also want source attachment, which is highly
recommended, just decompile it with your favourite tool (I prefer JD-GUI), and
save a source jar. Make sure that *"Realign line numbers"* or the equivalent
option is turned on.

Drop the jar in the mods folder of your workspace, manually add it to the project
dependencies after all the Gradle stuff, set its source attachment, and you're
ready. You can start your project with OptiFine thrown in the mix, debug,
set breakpoints, and everything.

**Note:** Debugging *into* one of the vanilla classes that OptiFine overwrites
may or may not give you some headache under Eclipse. I can only confirm it works
fine under IDEA, which allows you to switch between sources on the fly if multiple
jar files declare a class. A popup comes up saying *"Alternative source available
for the class blah blah blah"*, and you can switch to the OptiFine jar, which
contains the class actually executing.
34 changes: 34 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
apply plugin: 'kotlin'

group = 'com.octarine'
version = '0.5'

repositories { mavenCentral() }

buildscript {
ext.kotlin_version = '1.0.0'
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile 'org.ow2.asm:asm-debug-all:5.0.4'
compile 'net.sf.jopt-simple:jopt-simple:4.9'
}

jar {
manifest {
attributes "Main-Class": "com.octarine.simpledeobf.Main"
}

configurations.compile.each { dep ->
from(project.zipTree(dep)){
exclude 'META-INF', 'META-INF/**'
}
}
}
2 changes: 2 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
rootProject.name = 'simpledeobf'

99 changes: 99 additions & 0 deletions src/main/kotlin/com/octarine/simpledeobf/Main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
@file:JvmName("Main")
package com.octarine.simpledeobf

import joptsimple.OptionException
import joptsimple.OptionParser
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.tree.ClassNode
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream

fun main(args : Array<String>) {

try {
val parser = OptionParser()
val inputFile = parser.accepts("input", "Path to input JAR file").withRequiredArg().ofType(File::class.java).required()
val outputFile = parser.accepts("output", "Path to output JAR file").withRequiredArg().ofType(File::class.java).required()
val referenceFile = parser.accepts("ref", "Path to reference JAR file").withRequiredArg().ofType(File::class.java)
val mappingFile = parser.accepts("mapFile", "Path to mapping file").withRequiredArg().ofType(File::class.java)
val mapping = parser.accepts("map", "Manual mapping entry").withRequiredArg().ofType(String::class.java)
val defaultPkg = parser.accepts("defaultPkg", "Map default package").withRequiredArg().ofType(String::class.java)
val forcePublic = parser.accepts("forcePublic", "Force everything to be public")
val help = parser.acceptsAll(listOf("?", "help")).forHelp()

val options = parser.parse(*args)

if (options.has(help)) {
parser.printHelpOn(System.out)
System.exit(0)
}

if (options.valuesOf(outputFile).size != 1) {
println("Maximum of 1 output file is allowed")
System.exit(1)
}

if (options.valuesOf(defaultPkg).size > 1) {
println("Maximum of 1 default package is allowed")
System.exit(1)
}

val mapper = SimpleRemapper(options.valueOf(defaultPkg))
options.valuesOf(mappingFile).forEach {
println("Reading mappings from: ${it.absolutePath}")
mapper.readMappingFile(it)
}
options.valuesOf(mapping).forEach { mapper.readMappingLine(it) }
options.valuesOf(referenceFile).forEach {
println("Reading hiererchy from: ${it.absolutePath}")
mapper.hierarchyReader.visitAllFromFile(it)
}
options.valuesOf(inputFile).forEach {
println("Reading hiererchy from: ${it.absolutePath}")
mapper.hierarchyReader.visitAllFromFile(it)
}

val destJar = options.valuesOf(outputFile)[0].let {
if (it.exists()) it.delete()
ZipOutputStream(FileOutputStream(it))
}

for (srcFile in options.valuesOf(inputFile)) {
println("Processing input file: ${srcFile.absolutePath}")
val srcJar = ZipFile(srcFile)
for (srcEntry in srcJar.entries()) {
if (srcEntry.name.endsWith(".class")) {
print(" processing: ${srcEntry.name} ")
val srcBytes = srcJar.getInputStream(srcEntry).readBytes()
val srcClass = ClassNode().apply { ClassReader(srcBytes).accept(this, ClassReader.EXPAND_FRAMES) }
val destClass = mapper.remapClass(srcClass, options.has(forcePublic))
val destBytes = ClassWriter(0).apply { destClass.accept(this) }.toByteArray()

println(if (!srcEntry.name.startsWith(destClass.name)) "-> ${destClass.name}.class" else "")
destJar.putNextEntry(ZipEntry("${destClass.name}.class"))
destJar.write(destBytes)
destJar.closeEntry()
} else {
println(" copying: ${srcEntry.name}")
destJar.putNextEntry(ZipEntry(srcEntry.name))
srcJar.getInputStream(srcEntry).copyTo(destJar)
destJar.closeEntry()
}
}
}

println("Conversion finished")
destJar.close()
} catch(e: OptionException) {
println(e.message)
System.exit(1)
} catch(e: FileNotFoundException) {
println(e.message)
System.exit(1)
}
}
110 changes: 110 additions & 0 deletions src/main/kotlin/com/octarine/simpledeobf/Remapper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.octarine.simpledeobf

import org.objectweb.asm.*
import org.objectweb.asm.commons.Remapper
import org.objectweb.asm.commons.RemappingClassAdapter
import org.objectweb.asm.tree.ClassNode
import java.io.File
import java.util.*
import java.util.zip.ZipFile

class SimpleRemapper(val defaultPkg: String?) : Remapper() {

val String.partOwner: String get() = this.substring(0, this.lastIndexOf("/"))
val String.partName: String get() = this.substring(this.lastIndexOf("/") + 1)

val mappings = HashMap<String, ClassMapping>()
val hierarchy = HashMap<String, ClassHierarchy>()
val hierarchyReader = SimpleHierarchyReader()

inner class SimpleHierarchyReader : ClassVisitor(Opcodes.ASM5) {
override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array<out String>?) {
hierarchy.put(name, ClassHierarchy(superName, interfaces))
}

fun visitAllFromFile(file: File) {
val jarFile = ZipFile(file)
for (srcEntry in jarFile.entries()) {
if (srcEntry.name.endsWith(".class")) {
val srcBytes = jarFile.getInputStream(srcEntry).readBytes()
ClassReader(srcBytes).accept(this, ClassReader.EXPAND_FRAMES)
}
}
}
}

fun remapClass(srcClass: ClassNode, forcePublic: Boolean) = ClassNode().apply {
srcClass.accept(PublicAccessRemappingClassAdapter(this, this@SimpleRemapper, forcePublic))
}

fun readMappingFile(file: File) {
if (!file.exists()) throw Exception("Mappings file doesn't exist: ${file.absolutePath}")
file.readLines().forEach { readMappingLine(it) }
}

fun readMappingLine(line: String) {
val tokens = line.split(" ")
if (tokens[0] == "CL:") addClassMapping(tokens[1], tokens[2])
if (tokens[0] == "FD:") addFieldMapping(tokens[1].partOwner, tokens[1].partName, tokens[2].partOwner, tokens[2].partName)
if (tokens[0] == "MD:") addMethodMapping(tokens[1].partOwner, tokens[1].partName, tokens[2], tokens[3].partOwner, tokens[3].partName, tokens[4])
}

fun addClassMapping(fromName: String, toName: String) {
mappings.put(fromName, ClassMapping(toName))
}

fun addFieldMapping(fromOwner: String, fromName: String, toOwner: String, toName: String) {
mappings[fromOwner]?.fields?.put(fromName, toName)
}

fun addMethodMapping(fromOwner: String, fromName: String, fromDesc: String, toOwner: String, toName: String, toDesc: String) {
mappings[fromOwner]?.methods?.put(fromName to fromDesc, toName)
}

override fun map(typeName: String): String? = (mappings[typeName]?.mappedName ?: typeName).let {
if (it.contains("/") || defaultPkg == null) it else "$defaultPkg/$it"
}

override fun mapFieldName(owner: String, name: String, desc: String?): String? {
mappings[owner]?.let { it.fields[name] }?.let { return it }
hierarchy[owner]?.superName?.let { return mapFieldName(it, name, desc) }
return name
}

override fun mapMethodName(owner: String, name: String, desc: String) =
mapMethodNameInternal(owner, name, desc) ?: name

fun mapMethodNameInternal(owner: String, name: String, desc: String): String? {
mappings[owner]?.let { it.methods[name to desc] }?.let { return it }
val h = hierarchy[owner] ?: return null
if (h.superName != null) mapMethodNameInternal(h.superName, name, desc)?.let { return it }
if (h.interfaces != null) for (interfaceName in h.interfaces) {
mapMethodNameInternal(interfaceName, name, desc)?.let { return it }
}
return null
}
}

class PublicAccessRemappingClassAdapter(cv: ClassVisitor, remapper: Remapper, val force: Boolean) : RemappingClassAdapter(cv, remapper) {

val Int.toPublic: Int get() = (this and 0xFFF8) or 0x1

override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
return super.visitMethod(if (force) access.toPublic else access, name, desc, signature, exceptions)
}

override fun visitField(access: Int, name: String?, desc: String?, signature: String?, value: Any?): FieldVisitor? {
return super.visitField(if (force) access.toPublic else access, name, desc, signature, value)
}

override fun visitInnerClass(name: String?, outerName: String?, innerName: String?, access: Int) {
super.visitInnerClass(name, outerName, innerName, if (force) access.toPublic else access)
}
}

class ClassMapping(val mappedName: String) {
val fields = HashMap<String, String>()
val methods = HashMap<Pair<String, String>, String>()
}

class ClassHierarchy(val superName: String?, val interfaces: Array<out String>?) {}

0 comments on commit 61a0273

Please sign in to comment.