Files
runelite/src/main/java/info/sigterm/deob/deobfuscators/ConstantParameter.java

501 lines
14 KiB
Java

package info.sigterm.deob.deobfuscators;
import info.sigterm.deob.ClassGroup;
import info.sigterm.deob.Deobfuscator;
import info.sigterm.deob.Method;
import info.sigterm.deob.attributes.code.Instruction;
import info.sigterm.deob.attributes.code.Instructions;
import info.sigterm.deob.attributes.code.instruction.types.ComparisonInstruction;
import info.sigterm.deob.attributes.code.instruction.types.InvokeInstruction;
import info.sigterm.deob.attributes.code.instruction.types.JumpingInstruction;
import info.sigterm.deob.attributes.code.instruction.types.LVTInstruction;
import info.sigterm.deob.attributes.code.instruction.types.PushConstantInstruction;
import info.sigterm.deob.attributes.code.instructions.Goto;
import info.sigterm.deob.attributes.code.instructions.If;
import info.sigterm.deob.attributes.code.instructions.If0;
import info.sigterm.deob.execution.Execution;
import info.sigterm.deob.execution.Frame;
import info.sigterm.deob.execution.InstructionContext;
import info.sigterm.deob.execution.StackContext;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.apache.commons.collections4.map.MultiValueMap;
class ConstantMethodParameter
{
public List<Method> methods;
public int paramIndex;
public int lvtIndex;
public Object value;
@Override
public int hashCode()
{
int hash = 3;
hash = 47 * hash + Objects.hashCode(this.methods);
hash = 47 * hash + this.paramIndex;
hash = 47 * hash + Objects.hashCode(this.value);
return hash;
}
@Override
public boolean equals(Object obj)
{
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
final ConstantMethodParameter other = (ConstantMethodParameter) obj;
if (!Objects.equals(this.methods, other.methods))
{
return false;
}
if (this.paramIndex != other.paramIndex)
{
return false;
}
if (!Objects.equals(this.value, other.value))
{
return false;
}
return true;
}
}
class MethodGroup
{
public List<Method> methods; // methods that can be invoked
public Collection<Integer> constantParameters; // parameters which are always constant for all invocations
public List<ConstantMethodParameter> cmps = new ArrayList<>(); // cmps for all methods in the group, which hold the values.
}
public class ConstantParameter implements Deobfuscator
{
private List<ConstantMethodParameter> parameters = new ArrayList<>();
private MultiValueMap<Method, Integer> nonconst = new MultiValueMap<>(); // methods with non const parameters
private List<MethodGroup> methodGroups = new ArrayList<>();
private void checkMethodsAreConsistent(List<Method> methods)
{
Method prev = null;
for (Method m : methods)
{
if (prev != null)
{
assert prev.getDescriptor().equals(m.getDescriptor());
assert prev.isStatic() == m.isStatic();
}
prev = m;
}
}
private void findConstantParameter(Execution execution, List<Method> methods, InstructionContext invokeCtx)
{
checkMethodsAreConsistent(methods);
Method method = methods.get(0); // all methods must have the same signature etc
int offset = method.isStatic() ? 0 : 1;
List<StackContext> pops = invokeCtx.getPops();
outer:
// object is popped first, then param 1, 2, 3, etc. double and long take two slots.
for (int lvtOffset = offset, parameterIndex = 0;
parameterIndex < method.getDescriptor().size();
lvtOffset += method.getDescriptor().getTypeOfArg(parameterIndex++).getSlots())
{
// get(0) == first thing popped which is the last parameter,
// get(descriptor.size() - 1) == first parameter
StackContext ctx = pops.get(method.getDescriptor().size() - 1 - parameterIndex);
Collection<Integer> nonIdx = nonconst.getCollection(method);
boolean non = nonIdx != null && nonIdx.contains(parameterIndex);
if (non)
continue;
if (ctx.getPushed().getInstruction() instanceof PushConstantInstruction)
{
PushConstantInstruction pc = (PushConstantInstruction) ctx.getPushed().getInstruction();
if (!(pc.getConstant().getObject() instanceof Integer) && (!(pc.getConstant().getObject() instanceof Byte)))
continue;
ConstantMethodParameter cmp = new ConstantMethodParameter();
cmp.methods = methods;
cmp.paramIndex = parameterIndex;
cmp.lvtIndex = lvtOffset;
cmp.value = pc.getConstant().getObject();
// already exists?
for (ConstantMethodParameter c : parameters)
if (c.equals(cmp))
continue outer;
parameters.add(cmp);
}
else
{
nonconst.put(method, parameterIndex);
// remove from parameters as is not always const
for (Iterator<ConstantMethodParameter> it = parameters.iterator(); it.hasNext();)
{
ConstantMethodParameter c = it.next();
if (c.methods.equals(methods) && c.lvtIndex == lvtOffset)
{
it.remove();
}
}
}
}
}
private void findParameters(Execution execution)
{
for (Frame frame : execution.processedFrames)
for (InstructionContext ins : frame.getInstructions())
{
if (!(ins.getInstruction() instanceof InvokeInstruction))
continue;
List<Method> methods = ((InvokeInstruction) ins.getInstruction()).getMethods();
if (methods.isEmpty())
continue;
findConstantParameter(execution, methods, ins);
}
}
private boolean doLogicalComparison(Object value, ComparisonInstruction comparison, Object otherValue)
{
Instruction ins = (Instruction) comparison;
assert (comparison instanceof If0) == (otherValue == null);
assert otherValue == null || otherValue instanceof Integer;
switch (ins.getType())
{
case IFEQ:
return value.equals(0);
case IFNE:
return !value.equals(0);
case IFLT:
return (int) value < 0;
case IFGE:
return (int) value >= 0;
case IFGT:
return (int) value > 0;
case IFLE:
return (int) value <= 0;
case IF_ICMPEQ:
return value.equals(otherValue);
case IF_ICMPNE:
return !value.equals(otherValue);
case IF_ICMPLT:
return (int) value < (int) otherValue;
case IF_ICMPGE:
return (int) value >= (int) otherValue;
case IF_ICMPGT:
return (int) value > (int) otherValue;
case IF_ICMPLE:
return (int) value <= (int) otherValue;
default:
throw new RuntimeException("Unknown constant comparison instructuction");
}
}
private static class LogicallyDeadOp
{
InstructionContext compCtx; // logically dead comparison
boolean branch; // branch which is always taken
@Override
public boolean equals(Object obj)
{
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
final LogicallyDeadOp other = (LogicallyDeadOp) obj;
if (!Objects.equals(this.compCtx.getInstruction(), other.compCtx.getInstruction()))
{
return false;
}
if (this.branch != other.branch)
{
return false;
}
return true;
}
}
// find all comparisons of lvtIndex in method and record branch taken
private List<LogicallyDeadOp> isLogicallyDead(Execution execution, Method method, int lvtIndex, Object value)
{
List<LogicallyDeadOp> ops = new ArrayList<>();
for (Frame frame : execution.processedFrames)
{
if (frame.getMethod() != method)
continue;
for (InstructionContext ins : frame.getInstructions())
{
if (!(ins.getInstruction() instanceof ComparisonInstruction))
continue;
// assume that this will always be variable index #paramIndex comp with a constant.
ComparisonInstruction comp = (ComparisonInstruction) ins.getInstruction();
StackContext one, two = null;
if (comp instanceof If0)
{
one = ins.getPops().get(0);
}
else if (comp instanceof If)
{
one = ins.getPops().get(0);
two = ins.getPops().get(1);
}
else
{
throw new RuntimeException("Unknown comp ins");
}
// find if one is a lvt ins
LVTInstruction lvt = null;
StackContext other = null;
if (one.getPushed().getInstruction() instanceof LVTInstruction)
{
lvt = (LVTInstruction) one.getPushed().getInstruction();
other = two;
}
else if (two != null && two.getPushed().getInstruction() instanceof LVTInstruction)
{
lvt = (LVTInstruction) two.getPushed().getInstruction();
other = one;
}
assert lvt == null || !lvt.store();
if (lvt == null || lvt.getVariableIndex() != lvtIndex)
continue;
Object otherValue = null;
if (two != null) // two is null for if0
{
if (!(other.getPushed().getInstruction() instanceof PushConstantInstruction))
continue;
PushConstantInstruction pc = (PushConstantInstruction) other.getPushed().getInstruction();
otherValue = pc.getConstant().getObject();
}
// the result of the comparison doesn't matter, only that it always goes the same direction for every invocation
boolean result = doLogicalComparison(value, comp, otherValue);
LogicallyDeadOp deadOp = new LogicallyDeadOp();
deadOp.compCtx = ins;
deadOp.branch = result;
ops.add(deadOp);
}
}
return ops;
}
private static class MethodLvtPair
{
Method method;
int lvtIndex;
public MethodLvtPair(Method method, int lvtIndex)
{
this.method = method;
this.lvtIndex = lvtIndex;
}
@Override
public int hashCode()
{
int hash = 7;
hash = 41 * hash + Objects.hashCode(this.method);
hash = 41 * hash + this.lvtIndex;
return hash;
}
@Override
public boolean equals(Object obj)
{
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final MethodLvtPair other = (MethodLvtPair) obj;
if (!Objects.equals(this.method, other.method)) {
return false;
}
if (this.lvtIndex != other.lvtIndex) {
return false;
}
return true;
}
}
private Map<MethodLvtPair, List<LogicallyDeadOp> > deadops = new HashMap<>();
private Set<MethodLvtPair> invalidDeadops = new HashSet<>();
// check every method parameter that we've identified as being passed constants to see if it's logically dead
private void findLogicallyDeadOperations(Execution execution)
{
outer:
for (ConstantMethodParameter cmp : parameters)
{
for (Method method : cmp.methods)
{
MethodLvtPair pair = new MethodLvtPair(method, cmp.lvtIndex);
if (invalidDeadops.contains(pair))
continue;
// the dead comparisons must be the same and branch the same way for every call to this method.
List<LogicallyDeadOp> deadOps = isLogicallyDead(execution, method, cmp.lvtIndex, cmp.value);
// this must be per method,lvtindex
List<LogicallyDeadOp> existing = deadops.get(pair);
if (existing != null)
if (!existing.equals(deadOps))
{
// one of the branches taken differs because of the value, skip it
deadops.remove(pair);
invalidDeadops.add(pair);
continue;
}
else
{
continue;
}
deadops.put(pair, deadOps);
}
}
}
// remove logically dead comparisons
private int removeDeadOperations()
{
int count = 0;
for (MethodLvtPair mvp : deadops.keySet())
{
Method method = mvp.method;
List<LogicallyDeadOp> ops = deadops.get(mvp);
for (LogicallyDeadOp op : ops)
{
InstructionContext ctx = op.compCtx; // comparison
Instruction ins = ctx.getInstruction();
boolean branch = op.branch; // branch that is always taken
Instructions instructions = ins.getInstructions();
instructions.buildJumpGraph();
// remove the if
if (ctx.getInstruction() instanceof If)
ctx.removeStack(1);
ctx.removeStack(0);
int idx = instructions.getInstructions().indexOf(ins);
if (idx == -1)
continue; // already removed?
++count;
Instruction to;
if (branch)
{
JumpingInstruction jumpIns = (JumpingInstruction) ins;
assert jumpIns.getJumps().size() == 1;
to = jumpIns.getJumps().get(0);
}
else
{
// just go to next instruction
to = instructions.getInstructions().get(idx + 1);
}
assert to.getInstructions() == instructions;
assert ins != to;
assert instructions.getInstructions().contains(to);
// move things that jump here to instead jump to 'to'
for (Instruction fromI : ins.from)
{
assert fromI.jump.contains(ins);
fromI.jump.remove(ins);
fromI.replace(ins, to);
}
ins.from.clear();
instructions.remove(ins);
assert instructions.getInstructions().contains(to);
if (branch)
{
Goto gotoins = new Goto(instructions, to);
to.from.add(gotoins);
gotoins.jump.add(to);
// insert goto
instructions.getInstructions().add(idx, gotoins);
}
}
}
return count;
}
@Override
public void run(ClassGroup group)
{
group.buildClassGraph(); // required for getMethods in the invoke stuff by execution...
Execution execution = new Execution(group);
execution.populateInitialMethods();
execution.run();
findParameters(execution);
findLogicallyDeadOperations(execution);
int count = removeDeadOperations();
System.out.println("Removed " + count + " logically dead conditional jumps");
}
}