From 2cbdec17982445564e2fc9c10175298ee1995633 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 11 Mar 2016 14:31:31 -0500 Subject: [PATCH] Beginning work on inject replace --- .../java/net/runelite/deob/ClassFile.java | 13 + src/main/java/net/runelite/deob/Method.java | 10 + .../code/instructions/InvokeSpecial.java | 13 + .../attributes/code/instructions/New.java | 5 + .../rename/AnnotationMapper.java | 4 +- .../net/runelite/deob/injection/Inject.java | 4 +- .../deob/injection/InjectReplace.java | 322 ++++++++++++++++++ 7 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 src/main/java/net/runelite/deob/injection/InjectReplace.java diff --git a/src/main/java/net/runelite/deob/ClassFile.java b/src/main/java/net/runelite/deob/ClassFile.java index 29ae344a3d..7aed013180 100644 --- a/src/main/java/net/runelite/deob/ClassFile.java +++ b/src/main/java/net/runelite/deob/ClassFile.java @@ -14,6 +14,9 @@ import java.util.List; public class ClassFile { private static final int MAGIC = 0xcafebabe; + + private static final short ACC_FINAL = 0x0010; + private static final short ACC_ABSTRACT = 0x0400; private ClassGroup group; @@ -282,4 +285,14 @@ public class ClassFile { return this == other || interfaces.instanceOf(other) || (getParent() != null && getParent().instanceOf(other)); } + + public boolean isAbstract() + { + return (this.access_flags & ACC_ABSTRACT) != 0; + } + + public boolean isFinal() + { + return (this.access_flags & ACC_FINAL) != 0; + } } diff --git a/src/main/java/net/runelite/deob/Method.java b/src/main/java/net/runelite/deob/Method.java index 2182faf77d..c0e7a28766 100644 --- a/src/main/java/net/runelite/deob/Method.java +++ b/src/main/java/net/runelite/deob/Method.java @@ -135,6 +135,16 @@ public class Method { return (accessFlags & ACC_SYNCHRONIZED) != 0; } + + public boolean isFinal() + { + return (accessFlags & ACC_FINAL) != 0; + } + + public boolean isPrivate() + { + return (accessFlags & ACC_PRIVATE) != 0; + } public Exceptions getExceptions() { diff --git a/src/main/java/net/runelite/deob/attributes/code/instructions/InvokeSpecial.java b/src/main/java/net/runelite/deob/attributes/code/instructions/InvokeSpecial.java index 740e441102..a9ae3df712 100644 --- a/src/main/java/net/runelite/deob/attributes/code/instructions/InvokeSpecial.java +++ b/src/main/java/net/runelite/deob/attributes/code/instructions/InvokeSpecial.java @@ -38,6 +38,14 @@ public class InvokeSpecial extends Instruction implements InvokeInstruction { super(instructions, type, pc); } + + public InvokeSpecial(Instructions instructions, Method method) + { + super(instructions, InstructionType.INVOKESPECIAL, -1); + + this.method = method; + length += 2; + } @Override public void load(DataInputStream is) throws IOException @@ -136,6 +144,11 @@ public class InvokeSpecial extends Instruction implements InvokeInstruction { return method; } + + public void setMethod(Method method) + { + this.method = method; + } @Override public void lookup() diff --git a/src/main/java/net/runelite/deob/attributes/code/instructions/New.java b/src/main/java/net/runelite/deob/attributes/code/instructions/New.java index 0f3675a594..e3efcfd0c8 100644 --- a/src/main/java/net/runelite/deob/attributes/code/instructions/New.java +++ b/src/main/java/net/runelite/deob/attributes/code/instructions/New.java @@ -59,6 +59,11 @@ public class New extends Instruction { return clazz; } + + public void setNewClass(Class clazz) + { + this.clazz = clazz; + } @Override public void lookup() diff --git a/src/main/java/net/runelite/deob/deobfuscators/rename/AnnotationMapper.java b/src/main/java/net/runelite/deob/deobfuscators/rename/AnnotationMapper.java index 68e8b51644..a3624f0bbf 100644 --- a/src/main/java/net/runelite/deob/deobfuscators/rename/AnnotationMapper.java +++ b/src/main/java/net/runelite/deob/deobfuscators/rename/AnnotationMapper.java @@ -13,6 +13,8 @@ public class AnnotationMapper { private static final Type EXPORT = new Type("Lnet/runelite/mapping/Export;"); private static final Type IMPLEMENTS = new Type("Lnet/runelite/mapping/Implements;"); + private static final Type REPLACE = new Type("Lnet/runelite/mapping/Replace;"); + private static final Type OBFUSCATED_OVERRIDE = new Type("Lnet/runelite/mapping/ObfuscatedOverride;"); private final ClassGroup source, target; private final ParallelExecutorMapping mapping; @@ -114,6 +116,6 @@ public class AnnotationMapper private boolean isCopyable(Annotation a) { - return a.getType().equals(EXPORT) || a.getType().equals(IMPLEMENTS); + return a.getType().equals(EXPORT) || a.getType().equals(IMPLEMENTS) || a.getType().equals(REPLACE) || a.getType().equals(OBFUSCATED_OVERRIDE); } } diff --git a/src/main/java/net/runelite/deob/injection/Inject.java b/src/main/java/net/runelite/deob/injection/Inject.java index 76133911a9..116285363d 100644 --- a/src/main/java/net/runelite/deob/injection/Inject.java +++ b/src/main/java/net/runelite/deob/injection/Inject.java @@ -44,6 +44,8 @@ public class Inject private static final Type IMPLEMENTS = new Type("Lnet/runelite/mapping/Implements;"); private static final Type OBFUSCATED_GETTER = new Type("Lnet/runelite/mapping/ObfuscatedGetter;"); private static final Type OBFUSCATED_SIGNATURE = new Type("Lnet/runelite/mapping/ObfuscatedSignature;"); + + private static final String API_PACKAGE_BASE = "net.runelite.rs.api."; private static java.lang.Class clientClass; @@ -214,7 +216,7 @@ public class Inject if (a == null) return null; - String ifaceName = "net.runelite.rs.api." + a.getElement().getString(); + String ifaceName = API_PACKAGE_BASE + a.getElement().getString(); Class clazz = new Class(ifaceName); Interfaces interfaces = other.getInterfaces(); diff --git a/src/main/java/net/runelite/deob/injection/InjectReplace.java b/src/main/java/net/runelite/deob/injection/InjectReplace.java new file mode 100644 index 0000000000..37c9fbbb00 --- /dev/null +++ b/src/main/java/net/runelite/deob/injection/InjectReplace.java @@ -0,0 +1,322 @@ +package net.runelite.deob.injection; + +import java.io.DataInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import net.runelite.deob.ClassFile; +import net.runelite.deob.Method; +import net.runelite.deob.Methods; +import net.runelite.deob.attributes.Annotations; +import net.runelite.deob.attributes.Attributes; +import net.runelite.deob.attributes.Code; +import net.runelite.deob.attributes.annotation.Annotation; +import net.runelite.deob.attributes.code.Instruction; +import net.runelite.deob.attributes.code.InstructionType; +import net.runelite.deob.attributes.code.Instructions; +import net.runelite.deob.attributes.code.instructions.ALoad; +import net.runelite.deob.attributes.code.instructions.DLoad; +import net.runelite.deob.attributes.code.instructions.FLoad; +import net.runelite.deob.attributes.code.instructions.ILoad; +import net.runelite.deob.attributes.code.instructions.InvokeSpecial; +import net.runelite.deob.attributes.code.instructions.LLoad; +import net.runelite.deob.attributes.code.instructions.New; +import net.runelite.deob.attributes.code.instructions.Return; +import net.runelite.deob.signature.Type; + +public class InjectReplace +{ + private static final Type REPLACE = new Type("Lnet/runelite/mapping/Replace;"); + private static final Type OBFUSCATED_OVERRIDE = new Type("Lnet/runelite/mapping/ObfuscatedOverride;"); + private static final Type OBFUSCATED_NAME = new Type("Lnet/runelite/mapping/ObfuscatedName;"); + + private ClassFile cf, vanilla; + + public InjectReplace(ClassFile cf, ClassFile vanilla) + { + this.cf = cf; // deobfuscated class + this.vanilla = vanilla; // vanilla class + } + + public void run() throws ClassNotFoundException, IOException + { + Annotations an = cf.getAttributes().getAnnotations(); + if (an == null) + return; + + Annotation a = an.find(REPLACE); + if (a == null) + return; + + // cf = deobfuscated class with @Replace("net.runelite.whatever") + + // generate a new class. make it inherit from 'vanilla'. + + // make all classes which inherit from 'vanilla' instead inherit from the new class + // and adjust their constructors + + // add constructors to new class + // cf must implement an interface from the api? + // methods can have @ObfuscatedOverride("name") to be renamed + // to override ob'd method. + // replace all instances of new 'vanilla' with new 'new class' + + Class c = Class.forName(a.getElement().getString()); + ClassFile classToInject; + try (DataInputStream dis = new DataInputStream(c.getClassLoader().getResourceAsStream(c.getName().replace('.', '/') + ".class"))) + { + classToInject = new ClassFile(vanilla.getGroup(), dis); + vanilla.getGroup().addClass(classToInject); + } + + assert classToInject.getParentClass().getName().equals("java/lang/Object"); + assert classToInject.isAbstract(); + + // set parent + classToInject.setParentClass(vanilla.getPoolClass()); + assert !vanilla.isFinal(); + + injectConstructors(classToInject); + + overideMethods(classToInject); + + replaceSuperclass(classToInject); + + replaceNew(classToInject); + } + + private void injectConstructors(ClassFile classToInject) + { + // Delete compiler generate constructors + Methods methods = classToInject.getMethods(); + Methods vanillaMethods = vanilla.getMethods(); + + for (Method m : new ArrayList<>(methods.getMethods())) + if (m.getName().equals("")) + methods.removeMethod(m); + + // Add constructors + for (Method m : vanillaMethods.getMethods()) + if (m.getName().equals("")) + { + // create new constructor with same signature + Method constructor = new Method(methods, "", m.getDescriptor()); + constructor.setAccessFlags(Method.ACC_PUBLIC); + + Attributes methodAttributes = constructor.getAttributes(); + + // create code attribute + Code code = new Code(methodAttributes); + methodAttributes.addAttribute(code); + + Instructions instructions = code.getInstructions(); + List ins = instructions.getInstructions(); + + int index = 0; + ins.add(new ALoad(instructions, index++)); // this + + // push arguments + for (int i = 0; i < m.getDescriptor().size(); ++i) + { + Type type = m.getDescriptor().getTypeOfArg(i); + + if (type.getArrayDims() > 0 || !type.isPrimitive()) + { + ins.add(new ALoad(instructions, index++)); + } + else + { + switch (type.getType()) + { + case "B": + case "C": + case "I": + case "S": + case "Z": + ins.add(new ILoad(instructions, index++)); + break; + case "D": + ins.add(new DLoad(instructions, index++)); + ++index; // takes two slots + break; + case "F": + ins.add(new FLoad(instructions, index++)); + break; + case "J": + ins.add(new LLoad(instructions, index++)); + ++index; + break; + default: + throw new RuntimeException("Unknown type"); + } + } + } + + ins.add(new InvokeSpecial(instructions, m.getPoolMethod())); + ins.add(new Return(instructions, InstructionType.RETURN)); + + methods.addMethod(constructor); + } + } + + private void overideMethods(ClassFile classToInject) + { + // find methods in methods that are supposed to override obfuscated methods, and rename them. + + Methods methods = classToInject.getMethods(); + + for (Method m : methods.getMethods()) + { + Attributes attributes = m.getAttributes(); + Annotations annotations = attributes.getAnnotations(); + + if (annotations == null || annotations.find(OBFUSCATED_OVERRIDE) == null) + continue; + + Annotation annotation = annotations.find(OBFUSCATED_OVERRIDE); + String overridenMethod = annotation.getElement().getString(); // name of @Exported method to override + + // Find method with exported name on 'cf' + Method obfuscatedMethodToOverride = findMethodByObfuscatedName(overridenMethod); + + assert obfuscatedMethodToOverride != null; + assert !obfuscatedMethodToOverride.isFinal(); + assert !obfuscatedMethodToOverride.isPrivate(); + + // Rename method to override + m.setName(obfuscatedMethodToOverride.getName()); + + if (!m.getDescriptor().equals(obfuscatedMethodToOverride.getDescriptor())) + { + // Obfuscation can add garbage parameter. + assert m.getDescriptor().size() + 1 == obfuscatedMethodToOverride.getDescriptor().size(); + + // Either we have to modify the bytecode when it is copied over to include this, + // or maybe can inject overloaded function into superclass if it doesn't cause a signature collision + assert false; + } + + // This means method is overriden. It is possible that the return value is a child class + // of the parents overriden method, and it will still override the method however the signatures won't match, + // but we don't do that. + assert m.getDescriptor().equals(obfuscatedMethodToOverride.getDescriptor()); + + // Now that the function is overriden, when the invoke injector is called, it turns around and invokevirtuals + // the parent method, which hits ours. + } + } + + private Method findMethodByObfuscatedName(String name) + { + for (Method m : cf.getMethods().getMethods()) + { + Attributes attributes = m.getAttributes(); + Annotations annotations = attributes.getAnnotations(); + + if (annotations == null || annotations.find(OBFUSCATED_NAME) == null) + continue; + + Annotation annotation = annotations.find(OBFUSCATED_NAME); + String obfuscatedName = annotation.getElement().getString(); + + if (name.equals(obfuscatedName)) + return m; + } + + return null; + } + + private void replaceSuperclass(ClassFile classToInject) + { + // find all classes which inherit from 'vanilla'. replace with classToInject + + for (ClassFile cf : vanilla.getGroup().getClasses()) + if (cf.getParentClass().equals(vanilla.getPoolClass())) + { + cf.setParentClass(classToInject.getPoolClass()); + + // adjust constructors + + for (Method m : cf.getMethods().getMethods()) + { + if (!m.getName().equals("")) + continue; + + Code code = m.getCode(); + Instructions ins = code.getInstructions(); + + for (Instruction i : ins.getInstructions()) + { + if (!(i instanceof InvokeSpecial)) + continue; + + // The super constructor invokespecial will be the first invokespecial instruction encountered + InvokeSpecial is = (InvokeSpecial) i; + + net.runelite.deob.pool.Method method = (net.runelite.deob.pool.Method) is.getMethod(); + assert method.getClassEntry().equals(vanilla.getPoolClass()); + assert method.getNameAndType().getName().equals(""); + + is.setMethod( + new net.runelite.deob.pool.Method( + classToInject.getPoolClass(), + method.getNameAndType() + ) + ); + + break; + } + } + } + } + + private void replaceNew(ClassFile classToInject) + { + // new vanilla -> new classToInject + + for (ClassFile cf : vanilla.getGroup().getClasses()) + if (cf.getParentClass().equals(vanilla.getPoolClass())) + { + for (Method m : cf.getMethods().getMethods()) + { + Code code = m.getCode(); + Instructions ins = code.getInstructions(); + + boolean seen = false, isConstructor = m.getName().equals(""); + + for (Instruction i : ins.getInstructions()) + { + if (i instanceof New) + { + New n = (New) i; + if (!n.getNewClass().equals(vanilla.getPoolClass())) + continue; + + n.setNewClass(classToInject.getPoolClass()); + } + else if (i instanceof InvokeSpecial) + { + if (isConstructor) + { + if (!seen) + { + seen = true; + continue; // superclass invoke in constructor of class which inherits classToInject + } + } + + InvokeSpecial is = (InvokeSpecial) i; + net.runelite.deob.pool.Method method = (net.runelite.deob.pool.Method) is.getMethod(); + + is.setMethod( + new net.runelite.deob.pool.Method( + classToInject.getPoolClass(), + method.getNameAndType() + ) + ); + } + } + } + } + } +}