diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/AsmHelper.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/AsmHelper.java index ebb3ace..42789e7 100644 --- a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/AsmHelper.java +++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/AsmHelper.java @@ -318,6 +318,28 @@ public static List getInstructionsBetween( return instructions; } + public static AbstractInsnNode previousMeaningful(AbstractInsnNode insn) { + AbstractInsnNode current = insn == null ? null : insn.getPrevious(); + while (current != null && isStructural(current)) { + current = current.getPrevious(); + } + return current; + } + + public static AbstractInsnNode nextMeaningful(AbstractInsnNode insn) { + AbstractInsnNode current = insn == null ? null : insn.getNext(); + while (current != null && isStructural(current)) { + current = current.getNext(); + } + return current; + } + + public static boolean isStructural(AbstractInsnNode insn) { + return insn instanceof LabelNode + || insn.getType() == AbstractInsnNode.FRAME + || insn.getType() == AbstractInsnNode.LINE; + } + /** * Counts variable usages in the method. * diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/MethodHelper.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/MethodHelper.java index 4d6800c..4bbbbf8 100644 --- a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/MethodHelper.java +++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/MethodHelper.java @@ -7,8 +7,10 @@ import org.objectweb.asm.tree.analysis.BasicValue; import uwu.narumi.deobfuscator.api.asm.NamedOpcodes; import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.IincInsnNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.MethodNode; @@ -18,6 +20,7 @@ import org.objectweb.asm.tree.analysis.JumpPredictingAnalyzer; import org.objectweb.asm.tree.analysis.OriginalSourceInterpreter; import org.objectweb.asm.tree.analysis.OriginalSourceValue; +import uwu.narumi.deobfuscator.api.asm.FieldRef; import java.util.ArrayList; import java.util.Arrays; @@ -25,8 +28,10 @@ import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; public class MethodHelper implements Opcodes { @@ -172,4 +177,179 @@ public static int getFirstParameterIdx(MethodNode methodNode) { // When method is static, then the first var index is actually a reference to "this" return (methodNode.access & ACC_STATIC) != 0 ? 0 : 1; } + + public static Map collectKnownIntLocals( + MethodNode methodNode, + Map> frames, + Map fieldValues, + Integer methodSalt + ) { + Map locals = new HashMap<>(); + if (methodSalt != null && hasTrailingIntArgument(methodNode.desc)) { + locals.put(lastArgumentSlot(methodNode.access, methodNode.desc), methodSalt); + } + + boolean changed; + int rounds = 0; + do { + changed = false; + rounds++; + + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (!(insn instanceof VarInsnNode store) || store.getOpcode() != ISTORE) continue; + + Frame frame = frames.get(store); + if (frame == null || frame.getStackSize() == 0) continue; + + Optional value = evaluateInt(frame.getStack(frame.getStackSize() - 1), frames, locals, fieldValues); + if (value.isPresent() && !value.get().equals(locals.get(store.var))) { + locals.put(store.var, value.get()); + changed = true; + } + } + } while (changed && rounds < 6); + + return locals; + } + + public static Optional evaluateInt( + OriginalSourceValue source, + Map> frames, + Map locals, + Map fieldValues + ) { + if (source == null) return Optional.empty(); + if (source.getConstantValue() != null && source.getConstantValue().get() instanceof Number number) { + return Optional.of(number.intValue()); + } + if (!source.isOneWayProduced()) return Optional.empty(); + return evaluateInt(source.getProducer(), frames, locals, fieldValues, identitySet()); + } + + public static void removeStackProducers( + MethodNode methodNode, + AbstractInsnNode consumer, + Map> frames, + int count + ) { + Frame frame = frames.get(consumer); + if (frame == null || frame.getStackSize() < count) return; + + Set toRemove = identitySet(); + for (int i = 0; i < count; i++) { + OriginalSourceValue source = frame.getStack(frame.getStackSize() - 1 - i); + collectExpressionInsns(source, frames, toRemove, identitySet()); + } + + toRemove.stream() + .filter(insn -> insn != consumer) + .filter(methodNode.instructions::contains) + .sorted(Comparator.comparingInt(methodNode.instructions::indexOf).reversed()) + .forEach(methodNode.instructions::remove); + } + + public static List argumentSlots(int access, String desc) { + List slots = new ArrayList<>(); + int slot = (access & ACC_STATIC) != 0 ? 0 : 1; + for (Type arg : Type.getArgumentTypes(desc)) { + slots.add(slot); + slot += arg.getSize(); + } + return slots; + } + + public static boolean hasTrailingIntArgument(String desc) { + Type[] args = Type.getArgumentTypes(desc); + return args.length > 0 && args[args.length - 1].equals(Type.INT_TYPE); + } + + private static int lastArgumentSlot(int access, String desc) { + Type[] args = Type.getArgumentTypes(desc); + int slot = (access & ACC_STATIC) != 0 ? 0 : 1; + for (int i = 0; i < args.length - 1; i++) { + slot += args[i].getSize(); + } + return slot; + } + + private static Optional evaluateInt( + AbstractInsnNode producer, + Map> frames, + Map locals, + Map fieldValues, + Set visiting + ) { + if (producer == null || !visiting.add(producer)) return Optional.empty(); + + if (producer.isInteger()) return Optional.of(producer.asInteger()); + + if (producer instanceof FieldInsnNode field && field.getOpcode() == GETSTATIC && field.desc.equals("I")) { + return Optional.ofNullable(fieldValues.get(FieldRef.of(field))); + } + + if (producer instanceof VarInsnNode varInsn && varInsn.getOpcode() == ILOAD) { + return Optional.ofNullable(locals.get(varInsn.var)); + } + + if (producer.getOpcode() == INEG || producer.getOpcode() == I2B || producer.getOpcode() == I2C || producer.getOpcode() == I2S) { + Frame frame = frames.get(producer); + if (frame == null || frame.getStackSize() < 1) return Optional.empty(); + + Optional value = evaluateInt(frame.getStack(frame.getStackSize() - 1), frames, locals, fieldValues); + if (value.isEmpty()) return Optional.empty(); + + return switch (producer.getOpcode()) { + case INEG -> Optional.of(-value.get()); + case I2B -> Optional.of((int) value.get().byteValue()); + case I2C -> Optional.of((int) (char) value.get().intValue()); + case I2S -> Optional.of((int) value.get().shortValue()); + default -> Optional.empty(); + }; + } + + if (AsmMathHelper.isMathBinaryOperation(producer.getOpcode())) { + Frame frame = frames.get(producer); + if (frame == null || frame.getStackSize() < 2) return Optional.empty(); + + Optional left = evaluateInt(frame.getStack(frame.getStackSize() - 2), frames, locals, fieldValues); + Optional right = evaluateInt(frame.getStack(frame.getStackSize() - 1), frames, locals, fieldValues); + if (left.isEmpty() || right.isEmpty()) return Optional.empty(); + + try { + return Optional.of(AsmMathHelper.mathBinaryOperation(left.get(), right.get(), producer.getOpcode()).intValue()); + } catch (ArithmeticException ignored) { + return Optional.empty(); + } + } + + return Optional.empty(); + } + + private static void collectExpressionInsns( + OriginalSourceValue source, + Map> frames, + Set output, + Set visiting + ) { + if (source == null || !source.isOneWayProduced()) return; + + AbstractInsnNode producer = source.getProducer(); + if (producer == null || !visiting.add(producer)) return; + + if (producer instanceof VarInsnNode && producer.isVarStore()) return; + + output.add(producer); + + Frame frame = frames.get(producer); + if (frame == null) return; + + int consumed = producer.getConsumedStackValuesCount(frame); + for (int i = 0; i < consumed && i < frame.getStackSize(); i++) { + collectExpressionInsns(frame.getStack(frame.getStackSize() - 1 - i), frames, output, visiting); + } + } + + private static Set identitySet() { + return Collections.newSetFromMap(new IdentityHashMap<>()); + } } diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/composed/ComposedAidsfuscatorTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/composed/ComposedAidsfuscatorTransformer.java new file mode 100644 index 0000000..ad71f42 --- /dev/null +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/composed/ComposedAidsfuscatorTransformer.java @@ -0,0 +1,40 @@ +package uwu.narumi.deobfuscator.core.other.composed; + +import uwu.narumi.deobfuscator.api.transformer.ComposedTransformer; +import uwu.narumi.deobfuscator.core.other.composed.general.ComposedGeneralRepairTransformer; +import uwu.narumi.deobfuscator.core.other.composed.general.ComposedPeepholeCleanTransformer; +import uwu.narumi.deobfuscator.core.other.impl.aidsfuscator.AidsfuscatorClassSaltTransformer; +import uwu.narumi.deobfuscator.core.other.impl.aidsfuscator.AidsfuscatorConstantsFixTransformer; +import uwu.narumi.deobfuscator.core.other.impl.aidsfuscator.AidsfuscatorFlowFlatteningTransformer; +import uwu.narumi.deobfuscator.core.other.impl.aidsfuscator.AidsfuscatorIntegerTransformer; +import uwu.narumi.deobfuscator.core.other.impl.aidsfuscator.AidsfuscatorReferenceObfuscationTransformer; +import uwu.narumi.deobfuscator.core.other.impl.aidsfuscator.AidsfuscatorStringTransformer; +import uwu.narumi.deobfuscator.core.other.impl.universal.StringBuilderTransformer; +import uwu.narumi.deobfuscator.core.other.impl.universal.UniversalNumberTransformer; + +public class ComposedAidsfuscatorTransformer extends ComposedTransformer { + public ComposedAidsfuscatorTransformer() { + super( + ComposedGeneralRepairTransformer::new, + + AidsfuscatorFlowFlatteningTransformer::new, + AidsfuscatorReferenceObfuscationTransformer::new, + + AidsfuscatorClassSaltTransformer::new, + AidsfuscatorStringTransformer::new, + + UniversalNumberTransformer::new, + + AidsfuscatorIntegerTransformer::new, + UniversalNumberTransformer::new, + + AidsfuscatorConstantsFixTransformer::new, + + StringBuilderTransformer::new, + + () -> new ComposedTransformer(true, + UniversalNumberTransformer::new, + ComposedPeepholeCleanTransformer::new) + ); + } +} diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorClassSaltTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorClassSaltTransformer.java new file mode 100644 index 0000000..0204825 --- /dev/null +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorClassSaltTransformer.java @@ -0,0 +1,203 @@ +package uwu.narumi.deobfuscator.core.other.impl.aidsfuscator; + +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.OriginalSourceValue; +import uwu.narumi.deobfuscator.api.asm.ClassWrapper; +import uwu.narumi.deobfuscator.api.asm.FieldRef; +import uwu.narumi.deobfuscator.api.asm.MethodRef; +import uwu.narumi.deobfuscator.api.helper.AsmHelper; +import uwu.narumi.deobfuscator.api.helper.MethodHelper; +import uwu.narumi.deobfuscator.api.transformer.Transformer; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class AidsfuscatorClassSaltTransformer extends Transformer { + @Override + protected void transform() { + Map salts = findSalts(scopedClasses()); + if (salts.isEmpty()) return; + + scopedClasses().forEach(classWrapper -> classWrapper.methods().forEach(methodNode -> { + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (!(insn instanceof FieldInsnNode field) || field.getOpcode() != GETSTATIC || !field.desc.equals("I")) continue; + + Integer value = salts.get(FieldRef.of(field)); + if (value == null) continue; + + methodNode.instructions.set(field, AsmHelper.numberInsn(value)); + markChange(); + } + })); + + LOGGER.info("Inlined {} Aidsfuscator class salt loads", getChangesCount()); + } + + static Map findSalts(Collection classes) { + Map dispatcherValues = collectDispatcherSaltValues(classes); + if (dispatcherValues.isEmpty()) return Map.of(); + + Map salts = new HashMap<>(); + for (ClassWrapper classWrapper : classes) { + MethodNode clinit = classWrapper.findClInit().orElse(null); + if (clinit == null) continue; + + Map> frames; + try { + frames = MethodHelper.analyzeSource(classWrapper.classNode(), clinit); + } catch (RuntimeException ignored) { + continue; + } + + for (AbstractInsnNode insn : clinit.instructions.toArray()) { + if (!(insn instanceof FieldInsnNode putStatic) || putStatic.getOpcode() != PUTSTATIC || !putStatic.desc.equals("I")) { + continue; + } + + Frame frame = frames.get(putStatic); + if (frame == null || frame.getStackSize() == 0) continue; + + OriginalSourceValue value = frame.getStack(frame.getStackSize() - 1); + if (!value.isOneWayProduced() || !(value.getProducer() instanceof MethodInsnNode invoke)) continue; + if (invoke.getOpcode() != INVOKESTATIC || !invoke.desc.equals("(Ljava/lang/Object;I)I")) continue; + + MethodNode retriever = findMethod(classes, MethodRef.of(invoke)).orElse(null); + if (retriever == null || looksLikeOrderedClassSaltRetriever(retriever)) continue; + + Frame invokeFrame = frames.get(invoke); + if (invokeFrame == null || invokeFrame.getStackSize() < 2) continue; + + Optional fakeValue = MethodHelper.evaluateInt( + invokeFrame.getStack(invokeFrame.getStackSize() - 1), + frames, + Map.of(), + Map.of() + ); + if (fakeValue.isEmpty()) continue; + + Integer encodedSalt = dispatcherValues.get(fnv1a(classWrapper.canonicalName())); + if (encodedSalt == null) continue; + + salts.put(FieldRef.of(putStatic), encodedSalt ^ fakeValue.get()); + } + } + + return Map.copyOf(salts); + } + + static Map findMethodSalts(Collection classes, Map fieldValues) { + Map classMap = new HashMap<>(); + for (ClassWrapper classWrapper : classes) { + classMap.put(classWrapper.name(), classWrapper); + } + + Map salts = new HashMap<>(); + boolean changed; + int rounds = 0; + do { + changed = false; + rounds++; + + for (ClassWrapper classWrapper : classes) { + for (MethodNode methodNode : classWrapper.methods()) { + MethodRef caller = MethodRef.of(classWrapper.classNode(), methodNode); + Map> frames; + try { + frames = MethodHelper.analyzeSource(classWrapper.classNode(), methodNode); + } catch (RuntimeException ignored) { + continue; + } + + Map locals = MethodHelper.collectKnownIntLocals(methodNode, frames, fieldValues, salts.get(caller)); + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (!(insn instanceof MethodInsnNode call)) continue; + if (!MethodHelper.hasTrailingIntArgument(call.desc)) continue; + if (!classMap.containsKey(call.owner)) continue; + + Frame frame = frames.get(call); + if (frame == null || frame.getStackSize() < Type.getArgumentCount(call.desc)) continue; + + Optional salt = MethodHelper.evaluateInt(frame.getStack(frame.getStackSize() - 1), frames, locals, fieldValues); + if (salt.isEmpty()) continue; + + ClassWrapper owner = classMap.get(call.owner); + MethodNode target = owner.findMethod(call.name, call.desc).orElse(null); + if (target == null) continue; + + MethodRef targetRef = MethodRef.of(owner.classNode(), target); + if (!salts.containsKey(targetRef)) { + salts.put(targetRef, salt.get()); + changed = true; + } + } + } + } + } while (changed && rounds < 8); + + return Map.copyOf(salts); + } + + private static Map collectDispatcherSaltValues(Collection classes) { + Map values = new HashMap<>(); + + for (ClassWrapper classWrapper : classes) { + for (MethodNode methodNode : classWrapper.methods()) { + AbstractInsnNode[] insns = methodNode.instructions.toArray(); + for (int i = 0; i < insns.length; i++) { + if (!(insns[i] instanceof MethodInsnNode put) || put.getOpcode() != INVOKEINTERFACE) continue; + if (!put.owner.equals("java/util/Map") || !put.name.equals("put")) continue; + + Long classHash = null; + Integer value = null; + for (int j = Math.max(0, i - 12); j < i; j++) { + if (insns[j].isLong()) classHash = insns[j].asLong(); + if (insns[j].isInteger()) value = insns[j].asInteger(); + } + + if (classHash != null && value != null) { + values.put(classHash, value); + } + } + } + } + + return values; + } + + private static boolean looksLikeOrderedClassSaltRetriever(MethodNode methodNode) { + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (insn instanceof MethodInsnNode call + && call.owner.equals("java/lang/Throwable") + && call.name.equals("getStackTrace")) { + return true; + } + } + return false; + } + + private static Optional findMethod(Collection classes, MethodRef ref) { + for (ClassWrapper classWrapper : classes) { + ClassNode classNode = classWrapper.classNode(); + if (!classNode.name.equals(ref.owner())) continue; + return classWrapper.findMethod(ref.name(), ref.desc()); + } + return Optional.empty(); + } + + private static long fnv1a(String text) { + long hash = 0xcbf29ce484222325L; + for (int i = 0; i < text.length(); i++) { + hash ^= text.charAt(i); + hash *= 0x100000001b3L; + } + return hash; + } +} diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorConstantsFixTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorConstantsFixTransformer.java new file mode 100644 index 0000000..dc78d74 --- /dev/null +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorConstantsFixTransformer.java @@ -0,0 +1,102 @@ +package uwu.narumi.deobfuscator.core.other.impl.aidsfuscator; + +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.OriginalSourceValue; +import uwu.narumi.deobfuscator.api.asm.FieldRef; +import uwu.narumi.deobfuscator.api.helper.MethodHelper; +import uwu.narumi.deobfuscator.api.transformer.Transformer; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class AidsfuscatorConstantsFixTransformer extends Transformer { + @Override + protected void transform() { + scopedClasses().forEach(classWrapper -> { + MethodNode clinit = classWrapper.findClInit().orElse(null); + if (clinit == null) return; + + Map puts = countStaticPuts(classWrapper.name()); + + Map> frames; + try { + frames = MethodHelper.analyzeSource(classWrapper.classNode(), clinit); + } catch (RuntimeException ignored) { + return; + } + + for (AbstractInsnNode insn : clinit.instructions.toArray()) { + if (!(insn instanceof FieldInsnNode putStatic) || putStatic.getOpcode() != PUTSTATIC) continue; + if (!putStatic.owner.equals(classWrapper.name())) continue; + + FieldRef fieldRef = FieldRef.of(putStatic); + if (puts.getOrDefault(fieldRef, 0) != 1) continue; + + FieldNode fieldNode = classWrapper.findField(putStatic.name, putStatic.desc).orElse(null); + if (fieldNode == null || fieldNode.value != null) continue; + if ((fieldNode.access & ACC_STATIC) == 0 || (fieldNode.access & ACC_FINAL) == 0) continue; + + Object value = constantAssignedAt(putStatic, frames).orElse(null); + if (!isValidConstantValue(putStatic.desc, value)) continue; + + fieldNode.value = normalizeConstantValue(putStatic.desc, value); + MethodHelper.removeStackProducers(clinit, putStatic, frames, 1); + if (clinit.instructions.contains(putStatic)) { + clinit.instructions.remove(putStatic); + } + markChange(); + } + }); + + LOGGER.info("Recovered {} Aidsfuscator moved constant field values", getChangesCount()); + } + + private Map countStaticPuts(String owner) { + Map puts = new HashMap<>(); + scopedClasses().forEach(classWrapper -> classWrapper.methods().forEach(methodNode -> { + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (insn instanceof FieldInsnNode field && field.getOpcode() == PUTSTATIC && field.owner.equals(owner)) { + puts.merge(FieldRef.of(field), 1, Integer::sum); + } + } + })); + return puts; + } + + private Optional constantAssignedAt(FieldInsnNode putStatic, Map> frames) { + Frame frame = frames.get(putStatic); + if (frame == null || frame.getStackSize() == 0) return Optional.empty(); + + OriginalSourceValue source = frame.getStack(frame.getStackSize() - 1); + if (source.getConstantValue() == null) return Optional.empty(); + return Optional.ofNullable(source.getConstantValue().get()); + } + + private boolean isValidConstantValue(String desc, Object value) { + if (value == null) return false; + + Type type = Type.getType(desc); + return switch (type.getSort()) { + case Type.BOOLEAN, Type.CHAR, Type.BYTE, Type.SHORT, Type.INT -> value instanceof Number; + case Type.LONG -> value instanceof Long; + case Type.FLOAT -> value instanceof Float; + case Type.DOUBLE -> value instanceof Double; + case Type.OBJECT -> desc.equals("Ljava/lang/String;") && value instanceof String; + default -> false; + }; + } + + private Object normalizeConstantValue(String desc, Object value) { + Type type = Type.getType(desc); + if (type.getSort() == Type.BOOLEAN || type.getSort() == Type.CHAR || type.getSort() == Type.BYTE || type.getSort() == Type.SHORT || type.getSort() == Type.INT) { + return ((Number) value).intValue(); + } + return value; + } +} diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorFlowFlatteningTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorFlowFlatteningTransformer.java new file mode 100644 index 0000000..74ff97d --- /dev/null +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorFlowFlatteningTransformer.java @@ -0,0 +1,109 @@ +package uwu.narumi.deobfuscator.core.other.impl.aidsfuscator; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LookupSwitchInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.OriginalSourceValue; +import uwu.narumi.deobfuscator.api.helper.AsmHelper; +import uwu.narumi.deobfuscator.api.helper.MethodHelper; +import uwu.narumi.deobfuscator.api.transformer.Transformer; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class AidsfuscatorFlowFlatteningTransformer extends Transformer { + @Override + protected void transform() { + scopedClasses().forEach(classWrapper -> classWrapper.methods().forEach(methodNode -> { + if (methodNode.name.equals("")) return; + + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (!(insn instanceof LookupSwitchInsnNode lookupSwitch)) continue; + + Dispatcher dispatcher = identifyDispatcher(lookupSwitch); + if (dispatcher == null) continue; + + var frames = analyze(classWrapper.classNode(), methodNode); + if (frames == null) continue; + + List stores = new ArrayList<>(); + List jumps = new ArrayList<>(); + boolean allMatch = true; + + for (LabelNode target : lookupSwitch.labels) { + AbstractInsnNode jumpInsn = AsmHelper.previousMeaningful(target); + if (!(jumpInsn instanceof JumpInsnNode jump) || jump.getOpcode() != GOTO || jump.label != dispatcher.label) { + allMatch = false; + break; + } + + AbstractInsnNode storeInsn = AsmHelper.previousMeaningful(jumpInsn); + if (!(storeInsn instanceof VarInsnNode store) || store.getOpcode() != ISTORE || store.var != dispatcher.local) { + allMatch = false; + break; + } + + jumps.add(jumpInsn); + stores.add(storeInsn); + } + + if (!allMatch || stores.isEmpty()) continue; + + Set toRemove = new HashSet<>(jumps); + for (AbstractInsnNode storeInsn : stores) { + MethodHelper.removeStackProducers(methodNode, storeInsn, frames, 1); + toRemove.add(storeInsn); + } + + toRemove.forEach(node -> { + if (methodNode.instructions.contains(node)) { + methodNode.instructions.remove(node); + } + }); + + if (methodNode.instructions.contains(dispatcher.load)) { + methodNode.instructions.remove(dispatcher.load); + } + if (methodNode.instructions.contains(lookupSwitch)) { + methodNode.instructions.remove(lookupSwitch); + } + + markChange(); + } + })); + + LOGGER.info("Removed {} Aidsfuscator flow dispatcher groups", getChangesCount()); + } + + private Dispatcher identifyDispatcher(LookupSwitchInsnNode lookupSwitch) { + AbstractInsnNode previous = AsmHelper.previousMeaningful(lookupSwitch); + if (!(previous instanceof VarInsnNode load) || load.getOpcode() != ILOAD) return null; + + AbstractInsnNode raw = previous.getPrevious(); + while (raw != null && raw.getType() == AbstractInsnNode.FRAME) { + raw = raw.getPrevious(); + } + if (!(raw instanceof LabelNode label) || lookupSwitch.dflt != label) return null; + + if (lookupSwitch.labels == null || lookupSwitch.labels.size() < 3) return null; + return new Dispatcher(label, load.var, load); + } + + private java.util.Map> analyze(ClassNode classNode, MethodNode methodNode) { + try { + return MethodHelper.analyzeSource(classNode, methodNode); + } catch (RuntimeException ignored) { + return null; + } + } + + private record Dispatcher(LabelNode label, int local, VarInsnNode load) { + } +} diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorIntegerTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorIntegerTransformer.java new file mode 100644 index 0000000..46aae53 --- /dev/null +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorIntegerTransformer.java @@ -0,0 +1,337 @@ +package uwu.narumi.deobfuscator.core.other.impl.aidsfuscator; + +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.OriginalSourceValue; +import uwu.narumi.deobfuscator.api.asm.ClassWrapper; +import uwu.narumi.deobfuscator.api.asm.FieldRef; +import uwu.narumi.deobfuscator.api.asm.MethodRef; +import uwu.narumi.deobfuscator.api.helper.AsmHelper; +import uwu.narumi.deobfuscator.api.helper.MethodHelper; +import uwu.narumi.deobfuscator.api.transformer.Transformer; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +public class AidsfuscatorIntegerTransformer extends Transformer { + @Override + protected void transform() { + Collection classes = scopedClasses(); + Map classSalts = AidsfuscatorClassSaltTransformer.findSalts(classes); + Map methodSalts = AidsfuscatorClassSaltTransformer.findMethodSalts(classes, classSalts); + + Map decryptors = findDecryptors(classes); + if (decryptors.isEmpty()) return; + + Map pools = new HashMap<>(); + for (IntDecryptorInfo decryptor : decryptors.values()) { + ClassWrapper owner = findClass(classes, decryptor.methodRef.owner()).orElse(null); + if (owner == null || pools.containsKey(decryptor.numberField)) continue; + extractNumberPool(owner, decryptor.numberField, classSalts, methodSalts) + .ifPresent(pool -> pools.put(decryptor.numberField, pool)); + } + + Set usedDecryptors = new HashSet<>(); + for (ClassWrapper classWrapper : classes) { + for (MethodNode methodNode : classWrapper.methods()) { + MethodRef caller = MethodRef.of(classWrapper.classNode(), methodNode); + + Map> frames; + try { + frames = MethodHelper.analyzeSource(classWrapper.classNode(), methodNode); + } catch (RuntimeException ignored) { + continue; + } + + Map locals = MethodHelper.collectKnownIntLocals( + methodNode, + frames, + classSalts, + methodSalts.get(caller) + ); + + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (!(insn instanceof MethodInsnNode call) || call.getOpcode() != INVOKESTATIC) continue; + + MethodRef decryptorRef = MethodRef.of(call); + IntDecryptorInfo decryptor = decryptors.get(decryptorRef); + if (decryptor == null) continue; + + int[] pool = pools.get(decryptor.numberField); + if (pool == null) continue; + + Optional decrypted = decrypt(decryptor, pool, call, frames, locals, classSalts); + if (decrypted.isEmpty()) { + usedDecryptors.add(decryptorRef); + continue; + } + + MethodHelper.removeStackProducers(methodNode, call, frames, Type.getArgumentCount(call.desc)); + methodNode.instructions.set(call, AsmHelper.numberInsn(decrypted.get())); + markChange(); + } + } + } + + removeUnusedDecryptors(classes, decryptors.keySet(), usedDecryptors); + LOGGER.info("Resolved {} Aidsfuscator integer decryptor calls", getChangesCount()); + } + + private Map findDecryptors(Collection classes) { + Map decryptors = new HashMap<>(); + + for (ClassWrapper classWrapper : classes) { + for (MethodNode methodNode : classWrapper.methods()) { + if ((methodNode.access & ACC_STATIC) == 0) continue; + if (!methodNode.desc.equals("(II)I")) continue; + + extractDecryptor(classWrapper, methodNode).ifPresent(info -> decryptors.put(info.methodRef, info)); + } + } + + return decryptors; + } + + private Optional findClass(Collection classes, String name) { + for (ClassWrapper classWrapper : classes) { + if (classWrapper.name().equals(name)) return Optional.of(classWrapper); + } + return Optional.empty(); + } + + private Optional extractDecryptor(ClassWrapper classWrapper, MethodNode methodNode) { + FieldRef numberField = null; + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (insn instanceof FieldInsnNode field && field.getOpcode() == GETSTATIC && field.desc.equals("[I")) { + numberField = FieldRef.of(field); + break; + } + } + if (numberField == null) return Optional.empty(); + + XorInfo indexXor = null; + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (insn.getOpcode() != IXOR) continue; + + AbstractInsnNode constant = AsmHelper.previousMeaningful(insn); + AbstractInsnNode load = AsmHelper.previousMeaningful(constant); + AbstractInsnNode next = AsmHelper.nextMeaningful(insn); + if (!(load instanceof VarInsnNode loadVar) || loadVar.getOpcode() != ILOAD) continue; + if (constant == null || !constant.isInteger()) continue; + if (next == null || next.getOpcode() != IALOAD) continue; + + indexXor = new XorInfo(loadVar.var, constant.asInteger()); + break; + } + if (indexXor == null) return Optional.empty(); + + List masks = extractMasks(methodNode); + return Optional.of(new IntDecryptorInfo( + MethodRef.of(classWrapper.classNode(), methodNode), + numberField, + indexXor.constant, + masks + )); + } + + private Optional extractNumberPool( + ClassWrapper classWrapper, + FieldRef numberField, + Map fieldValues, + Map methodSalts + ) { + MethodNode clinit = classWrapper.findClInit().orElse(null); + if (clinit == null) return Optional.empty(); + + Map> frames; + try { + frames = MethodHelper.analyzeSource(classWrapper.classNode(), clinit); + } catch (RuntimeException ignored) { + return Optional.empty(); + } + + Map locals = MethodHelper.collectKnownIntLocals( + clinit, + frames, + fieldValues, + methodSalts.get(MethodRef.of(classWrapper.classNode(), clinit)) + ); + + for (AbstractInsnNode insn : clinit.instructions.toArray()) { + if (!(insn instanceof FieldInsnNode putStatic) || putStatic.getOpcode() != PUTSTATIC) continue; + if (!FieldRef.of(putStatic).equals(numberField)) continue; + + PoolSeed seed = findPoolSeed(clinit, frames, locals, fieldValues, putStatic).orElse(null); + if (seed == null) continue; + + byte[] bytes = seed.encoded.getBytes(StandardCharsets.ISO_8859_1); + if (bytes.length % 4 != 0) continue; + + int[] pool = new int[bytes.length / 4]; + for (int i = 0; i < pool.length; i++) { + int offset = i * 4; + int value = ((bytes[offset] & 0xff) << 24) + | ((bytes[offset + 1] & 0xff) << 16) + | ((bytes[offset + 2] & 0xff) << 8) + | (bytes[offset + 3] & 0xff); + pool[i] = value ^ seed.key; + } + + return Optional.of(pool); + } + + return Optional.empty(); + } + + private Optional findPoolSeed( + MethodNode clinit, + Map> frames, + Map locals, + Map fieldValues, + FieldInsnNode putStatic + ) { + AbstractInsnNode encodedInsn = null; + AbstractInsnNode cursor = putStatic; + while ((cursor = cursor.getPrevious()) != null) { + if (cursor instanceof LdcInsnNode ldc + && ldc.cst instanceof String + && AsmHelper.nextMeaningful(cursor) instanceof LdcInsnNode charset + && charset.cst instanceof String + && charset.cst.equals("ISO-8859-1")) { + encodedInsn = cursor; + break; + } + } + if (!(encodedInsn instanceof LdcInsnNode encodedLdc)) return Optional.empty(); + + AbstractInsnNode keyStore = encodedInsn; + while ((keyStore = keyStore.getPrevious()) != null) { + if (!(keyStore instanceof VarInsnNode store) || store.getOpcode() != ISTORE) continue; + + Frame frame = frames.get(store); + if (frame == null || frame.getStackSize() == 0) continue; + + Optional key = MethodHelper.evaluateInt( + frame.getStack(frame.getStackSize() - 1), + frames, + locals, + fieldValues + ); + if (key.isPresent()) { + return Optional.of(new PoolSeed((String) encodedLdc.cst, key.get())); + } + } + + return Optional.empty(); + } + + private Optional decrypt( + IntDecryptorInfo decryptor, + int[] pool, + MethodInsnNode call, + Map> frames, + Map locals, + Map fieldValues + ) { + Frame frame = frames.get(call); + if (frame == null || frame.getStackSize() < 2) return Optional.empty(); + + Optional idxArg = MethodHelper.evaluateInt(frame.getStack(frame.getStackSize() - 2), frames, locals, fieldValues); + Optional keyArg = MethodHelper.evaluateInt(frame.getStack(frame.getStackSize() - 1), frames, locals, fieldValues); + if (idxArg.isEmpty() || keyArg.isEmpty()) return Optional.empty(); + + int index = idxArg.get() ^ decryptor.indexXor; + if (index < 0 || index >= pool.length) return Optional.empty(); + + int value = pool[index] ^ keyArg.get() ^ idxArg.get(); + for (IntMask mask : decryptor.masks) { + value = mask.apply(value); + } + + return Optional.of(value); + } + + private List extractMasks(MethodNode methodNode) { + AbstractInsnNode valueLoad = null; + boolean seenArrayLoad = false; + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (insn.getOpcode() == IALOAD) { + seenArrayLoad = true; + continue; + } + if (seenArrayLoad && insn instanceof VarInsnNode load && load.getOpcode() == ILOAD) { + valueLoad = load; + break; + } + } + if (valueLoad == null) return List.of(); + + List masks = new ArrayList<>(); + AbstractInsnNode cursor = valueLoad; + while ((cursor = AsmHelper.nextMeaningful(cursor)) != null && cursor.getOpcode() != IRETURN) { + int opcode = cursor.getOpcode(); + if (opcode != IADD && opcode != ISUB && opcode != IXOR) continue; + + AbstractInsnNode constant = AsmHelper.previousMeaningful(cursor); + if (constant == null || !constant.isInteger()) continue; + masks.add(new IntMask(opcode, constant.asInteger())); + } + + return masks; + } + + private void removeUnusedDecryptors(Collection classes, Set decryptors, Set stillUsed) { + Set referenced = new HashSet<>(stillUsed); + for (ClassWrapper classWrapper : classes) { + for (MethodNode methodNode : classWrapper.methods()) { + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (insn instanceof MethodInsnNode call) { + MethodRef ref = MethodRef.of(call); + if (decryptors.contains(ref)) referenced.add(ref); + } + } + } + } + + for (ClassWrapper classWrapper : classes) { + boolean removed = classWrapper.methods().removeIf(method -> { + MethodRef ref = MethodRef.of(classWrapper.classNode(), method); + return decryptors.contains(ref) && !referenced.contains(ref); + }); + if (removed) markChange(); + } + } + + private record IntDecryptorInfo(MethodRef methodRef, FieldRef numberField, int indexXor, List masks) { + } + + private record PoolSeed(String encoded, int key) { + } + + private record XorInfo(int local, int constant) { + } + + private record IntMask(int opcode, int value) { + int apply(int input) { + return switch (opcode) { + case IADD -> input + value; + case ISUB -> input - value; + case IXOR -> input ^ value; + default -> input; + }; + } + } +} diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorReferenceObfuscationTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorReferenceObfuscationTransformer.java new file mode 100644 index 0000000..6854b03 --- /dev/null +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorReferenceObfuscationTransformer.java @@ -0,0 +1,368 @@ +package uwu.narumi.deobfuscator.core.other.impl.aidsfuscator; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.InvokeDynamicInsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.LookupSwitchInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.OriginalSourceValue; +import uwu.narumi.deobfuscator.api.asm.ClassWrapper; +import uwu.narumi.deobfuscator.api.asm.FieldRef; +import uwu.narumi.deobfuscator.api.asm.MethodRef; +import uwu.narumi.deobfuscator.api.helper.AsmHelper; +import uwu.narumi.deobfuscator.api.helper.MethodHelper; +import uwu.narumi.deobfuscator.api.transformer.Transformer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +public class AidsfuscatorReferenceObfuscationTransformer extends Transformer { + @Override + protected void transform() { + Collection classes = scopedClasses(); + + List dispatchers = findDispatchers(classes); + if (dispatchers.isEmpty()) return; + + Map dispatchersByBootstrapOwner = new HashMap<>(); + for (ReferenceDispatcher dispatcher : dispatchers) { + dispatchersByBootstrapOwner.put(dispatcher.owner.name(), dispatcher); + } + + boolean resolvedAny; + do { + resolvedAny = resolvePass(classes, dispatchersByBootstrapOwner); + } while (resolvedAny); + + removeResolvedDispatchers(classes, dispatchers, dispatchersByBootstrapOwner); + LOGGER.info("Resolved {} Aidsfuscator invokedynamic references", getChangesCount()); + } + + private boolean resolvePass(Collection classes, Map dispatchersByBootstrapOwner) { + Map classSalts = AidsfuscatorClassSaltTransformer.findSalts(classes); + Map methodSalts = AidsfuscatorClassSaltTransformer.findMethodSalts(classes, classSalts); + + boolean resolvedAny = false; + for (ClassWrapper classWrapper : classes) { + for (MethodNode methodNode : classWrapper.methods()) { + MethodRef caller = MethodRef.of(classWrapper.classNode(), methodNode); + + Map> frames; + try { + frames = MethodHelper.analyzeSource(classWrapper.classNode(), methodNode); + } catch (RuntimeException ignored) { + continue; + } + + Map locals = MethodHelper.collectKnownIntLocals( + methodNode, + frames, + classSalts, + methodSalts.get(caller) + ); + + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (!(insn instanceof InvokeDynamicInsnNode indy)) continue; + + ReferenceDispatcher dispatcher = dispatchersByBootstrapOwner.get(indy.bsm.getOwner()); + if (dispatcher == null) continue; + + Optional replacement = resolveReference(dispatcher, indy, frames, locals, classSalts); + if (replacement.isEmpty()) continue; + + MethodHelper.removeStackProducers(methodNode, indy, frames, 2); + methodNode.instructions.set(indy, replacement.get()); + markChange(); + resolvedAny = true; + } + } + } + return resolvedAny; + } + + private void removeResolvedDispatchers( + Collection classes, + List dispatchers, + Map dispatchersByBootstrapOwner + ) { + Set stillReferenced = new HashSet<>(); + for (ClassWrapper classWrapper : classes) { + for (MethodNode methodNode : classWrapper.methods()) { + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (insn instanceof InvokeDynamicInsnNode indy && dispatchersByBootstrapOwner.containsKey(indy.bsm.getOwner())) { + stillReferenced.add(indy.bsm.getOwner()); + } + } + } + } + + for (ReferenceDispatcher dispatcher : dispatchers) { + if (!stillReferenced.contains(dispatcher.owner.name())) { + context().getClassesMap().remove(dispatcher.owner.name()); + markChange(); + } + } + } + + private List findDispatchers(Collection classes) { + List dispatchers = new ArrayList<>(); + + for (ClassWrapper classWrapper : classes) { + MethodNode bootstrap = classWrapper.findMethod( + null, + "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;" + ).orElse(null); + if (bootstrap == null) continue; + + MethodNode mainInvoker = classWrapper.findMethod( + null, + "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/invoke/MutableCallSite;Ljava/lang/String;Ljava/lang/invoke/MethodType;II)Ljava/lang/invoke/MethodHandle;" + ).orElse(null); + if (mainInvoker == null) continue; + + FieldRef refField = classWrapper.fields().stream() + .filter(field -> field.desc.equals("[Ljava/lang/String;")) + .map(field -> FieldRef.of(classWrapper.classNode(), field)) + .findFirst() + .orElse(null); + if (refField == null) continue; + + Integer indexKey = extractIndexKey(mainInvoker).orElse(null); + if (indexKey == null) continue; + + Map kinds = extractReferenceKinds(mainInvoker); + if (kinds.size() < 6) continue; + + String[] references = extractReferences(classWrapper, refField); + if (references.length == 0) continue; + + dispatchers.add(new ReferenceDispatcher(classWrapper, refField, indexKey, kinds, references)); + } + + return dispatchers; + } + + private Optional extractIndexKey(MethodNode mainInvoker) { + for (AbstractInsnNode insn : mainInvoker.instructions.toArray()) { + if (insn.getOpcode() != IXOR) continue; + + AbstractInsnNode constant = AsmHelper.previousMeaningful(insn); + AbstractInsnNode load = AsmHelper.previousMeaningful(constant); + AbstractInsnNode next = AsmHelper.nextMeaningful(insn); + if (!(load instanceof VarInsnNode varInsn) || varInsn.getOpcode() != ILOAD) continue; + if (constant == null || !constant.isInteger()) continue; + if (!(next instanceof VarInsnNode store) || store.getOpcode() != ISTORE) continue; + + return Optional.of(constant.asInteger()); + } + return Optional.empty(); + } + + private Map extractReferenceKinds(MethodNode mainInvoker) { + Map kinds = new HashMap<>(); + + LookupSwitchInsnNode lookupSwitch = null; + for (AbstractInsnNode insn : mainInvoker.instructions.toArray()) { + if (insn instanceof LookupSwitchInsnNode sw) { + lookupSwitch = sw; + break; + } + } + if (lookupSwitch == null) return kinds; + + Map> keysByLabel = new HashMap<>(); + for (int i = 0; i < lookupSwitch.keys.size(); i++) { + keysByLabel.computeIfAbsent(lookupSwitch.labels.get(i), ignored -> new ArrayList<>()).add(lookupSwitch.keys.get(i)); + } + + for (Map.Entry> entry : keysByLabel.entrySet()) { + LabelScan scan = scanLabel(entry.getKey()); + if (scan.kind != null) { + for (Integer key : entry.getValue()) { + kinds.put(key, scan.kind); + } + continue; + } + + if (scan.getterChar == null || scan.fieldBaseKind == null) continue; + for (Integer key : entry.getValue()) { + if (key.equals(scan.getterChar)) { + kinds.put(key, scan.fieldBaseKind == ReferenceKind.INSTANCE_FIELD ? ReferenceKind.GET_FIELD : ReferenceKind.GET_STATIC); + } else { + kinds.put(key, scan.fieldBaseKind == ReferenceKind.INSTANCE_FIELD ? ReferenceKind.PUT_FIELD : ReferenceKind.PUT_STATIC); + } + } + } + + return kinds; + } + + private LabelScan scanLabel(LabelNode label) { + AbstractInsnNode cursor = label; + Integer getterChar = null; + ReferenceKind fieldBaseKind = null; + + int guard = 0; + while ((cursor = AsmHelper.nextMeaningful(cursor)) != null && guard++ < 80) { + if (cursor instanceof MethodInsnNode call && call.owner.equals("java/lang/invoke/MethodHandles$Lookup")) { + switch (call.name) { + case "findVirtual": + return new LabelScan(ReferenceKind.INVOKE_VIRTUAL, null, null); + case "findStatic": + return new LabelScan(ReferenceKind.INVOKE_STATIC, null, null); + case "findGetter", "findSetter": + if (fieldBaseKind == null) fieldBaseKind = ReferenceKind.INSTANCE_FIELD; + break; + case "findStaticGetter", "findStaticSetter": + if (fieldBaseKind == null) fieldBaseKind = ReferenceKind.STATIC_FIELD; + break; + default: + break; + } + } + + if (cursor instanceof JumpInsnNode jump && jump.getOpcode() == IF_ICMPEQ) { + AbstractInsnNode constant = AsmHelper.previousMeaningful(cursor); + if (constant != null && constant.isInteger() && getterChar == null) getterChar = constant.asInteger(); + } + + if (cursor.getOpcode() == GOTO && fieldBaseKind != null) break; + } + + return new LabelScan(null, getterChar, fieldBaseKind); + } + + private String[] extractReferences(ClassWrapper classWrapper, FieldRef refField) { + Map values = new HashMap<>(); + int max = -1; + + for (MethodNode methodNode : classWrapper.methods()) { + if (!loadsRefField(methodNode, refField)) continue; + + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (insn.getOpcode() != AASTORE) continue; + + AbstractInsnNode value = AsmHelper.previousMeaningful(insn); + AbstractInsnNode index = AsmHelper.previousMeaningful(value); + if (!(value instanceof LdcInsnNode ldc) || !(ldc.cst instanceof String text)) continue; + if (index == null || !index.isInteger()) continue; + + values.put(index.asInteger(), text); + max = Math.max(max, index.asInteger()); + } + } + + if (max < 0) return new String[0]; + String[] refs = new String[max + 1]; + for (Map.Entry entry : values.entrySet()) { + refs[entry.getKey()] = entry.getValue(); + } + return refs; + } + + private boolean loadsRefField(MethodNode methodNode, FieldRef refField) { + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (insn instanceof FieldInsnNode field && field.getOpcode() == GETSTATIC && FieldRef.of(field).equals(refField)) { + return true; + } + } + return false; + } + + private Optional resolveReference( + ReferenceDispatcher dispatcher, + InvokeDynamicInsnNode indy, + Map> frames, + Map locals, + Map fieldValues + ) { + ReferenceKind kind = dispatcher.kinds.get((int) indy.name.charAt(0)); + if (kind == null) return Optional.empty(); + + Frame frame = frames.get(indy); + if (frame == null || frame.getStackSize() < 2) return Optional.empty(); + + Optional idxArg = MethodHelper.evaluateInt(frame.getStack(frame.getStackSize() - 2), frames, locals, fieldValues); + Optional keyArg = MethodHelper.evaluateInt(frame.getStack(frame.getStackSize() - 1), frames, locals, fieldValues); + if (idxArg.isEmpty() || keyArg.isEmpty()) return Optional.empty(); + + int index = idxArg.get() ^ dispatcher.indexKey; + if (index < 0 || index >= dispatcher.references.length || dispatcher.references[index] == null) return Optional.empty(); + + int decryptKey = keyArg.get() >>> 16; + String token = xor(dispatcher.references[index], decryptKey, 0); + String[] parts = token.split(":", 3); + if (parts.length != 3) return Optional.empty(); + + String owner = parts[0].replace('.', '/'); + String name = parts[1]; + String desc = parts[2]; + + return switch (kind) { + case INVOKE_STATIC -> Optional.of(new MethodInsnNode(INVOKESTATIC, owner, name, desc, false)); + case INVOKE_VIRTUAL -> { + int opcode = isInterface(owner) ? INVOKEINTERFACE : INVOKEVIRTUAL; + yield Optional.of(new MethodInsnNode(opcode, owner, name, desc, opcode == INVOKEINTERFACE)); + } + case GET_STATIC -> Optional.of(new FieldInsnNode(GETSTATIC, owner, name, fieldDesc(desc))); + case PUT_STATIC -> Optional.of(new FieldInsnNode(PUTSTATIC, owner, name, fieldDesc(desc))); + case GET_FIELD -> Optional.of(new FieldInsnNode(GETFIELD, owner, name, fieldDesc(desc))); + case PUT_FIELD -> Optional.of(new FieldInsnNode(PUTFIELD, owner, name, fieldDesc(desc))); + case INSTANCE_FIELD, STATIC_FIELD -> Optional.empty(); + }; + } + + private String xor(String text, int key1, int key2) { + char[] chars = text.toCharArray(); + for (int i = 0; i < chars.length; i++) { + chars[i] = (char) (chars[i] ^ key1 ^ key2); + } + return new String(chars); + } + + private String fieldDesc(String tokenDesc) { + return tokenDesc.startsWith("()") ? tokenDesc.substring(2) : tokenDesc; + } + + private boolean isInterface(String owner) { + ClassNode info = context().getFullClassProvider().getClassInfo(owner); + return info != null && (info.access & Opcodes.ACC_INTERFACE) != 0; + } + + private record ReferenceDispatcher( + ClassWrapper owner, + FieldRef refField, + int indexKey, + Map kinds, + String[] references + ) { + } + + private enum ReferenceKind { + INVOKE_VIRTUAL, + INVOKE_STATIC, + GET_FIELD, + GET_STATIC, + PUT_FIELD, + PUT_STATIC, + INSTANCE_FIELD, + STATIC_FIELD + } + + private record LabelScan(ReferenceKind kind, Integer getterChar, ReferenceKind fieldBaseKind) { + } +} diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorStringTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorStringTransformer.java new file mode 100644 index 0000000..dece5a6 --- /dev/null +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/aidsfuscator/AidsfuscatorStringTransformer.java @@ -0,0 +1,1095 @@ +package uwu.narumi.deobfuscator.core.other.impl.aidsfuscator; + +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.IincInsnNode; +import org.objectweb.asm.tree.IntInsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.LookupSwitchInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TableSwitchInsnNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.VarInsnNode; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.OriginalSourceValue; +import uwu.narumi.deobfuscator.api.asm.ClassWrapper; +import uwu.narumi.deobfuscator.api.asm.FieldRef; +import uwu.narumi.deobfuscator.api.asm.MethodRef; +import uwu.narumi.deobfuscator.api.helper.AsmHelper; +import uwu.narumi.deobfuscator.api.helper.MethodHelper; +import uwu.narumi.deobfuscator.api.transformer.Transformer; + +import java.lang.reflect.Array; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +public class AidsfuscatorStringTransformer extends Transformer { + @Override + protected void transform() { + Collection classes = scopedClasses(); + Map classSalts = AidsfuscatorClassSaltTransformer.findSalts(classes); + Map methodSalts = AidsfuscatorClassSaltTransformer.findMethodSalts(classes, classSalts); + + Map decryptors = findDecryptors(classes); + if (decryptors.isEmpty()) return; + + Map pools = new HashMap<>(); + for (DecryptorInfo decryptor : decryptors.values()) { + ClassWrapper owner = findClass(classes, decryptor.methodRef().owner()).orElse(null); + if (owner == null) continue; + + if (!pools.containsKey(decryptor.stringField())) { + extractStringPool(owner, decryptor.stringField(), classSalts, methodSalts) + .ifPresent(pool -> pools.put(decryptor.stringField(), pool)); + } + } + + LOGGER.info("Identified {} Aidsfuscator string decryptors and {} string pools", decryptors.size(), pools.size()); + + Set usedDecryptors = new HashSet<>(); + for (ClassWrapper classWrapper : classes) { + for (MethodNode methodNode : classWrapper.methods()) { + MethodRef caller = MethodRef.of(classWrapper.classNode(), methodNode); + + Map> frames; + try { + frames = MethodHelper.analyzeSource(classWrapper.classNode(), methodNode); + } catch (RuntimeException ignored) { + continue; + } + + Map locals = MethodHelper.collectKnownIntLocals( + methodNode, + frames, + classSalts, + methodSalts.get(caller) + ); + + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (!(insn instanceof MethodInsnNode call) || call.getOpcode() != INVOKESTATIC) continue; + + MethodRef decryptorRef = MethodRef.of(call); + DecryptorInfo decryptor = decryptors.get(decryptorRef); + if (decryptor == null) continue; + + String[] pool = pools.get(decryptor.stringField()); + if (pool == null) continue; + + Optional decrypted = decrypt(decryptor, pool, classWrapper, methodNode, call, frames, locals, classSalts); + if (decrypted.isEmpty()) { + usedDecryptors.add(decryptorRef); + continue; + } + + MethodHelper.removeStackProducers(methodNode, call, frames, Type.getArgumentCount(call.desc)); + methodNode.instructions.set(call, new LdcInsnNode(decrypted.get())); + markChange(); + } + } + } + + removeUnusedDecryptors(classes, decryptors.keySet(), usedDecryptors); + LOGGER.info("Resolved {} Aidsfuscator string decryptor calls", getChangesCount()); + } + + private Map findDecryptors(Collection classes) { + Map decryptors = new HashMap<>(); + + for (ClassWrapper classWrapper : classes) { + for (MethodNode methodNode : classWrapper.methods()) { + if ((methodNode.access & ACC_STATIC) == 0) continue; + if (!Type.getReturnType(methodNode.desc).equals(Type.getType(String.class))) continue; + + DecryptorInfo info = null; + if (methodNode.desc.equals("(II)Ljava/lang/String;")) { + info = extractDefaultDecryptor(classWrapper, methodNode).orElse(null); + } + if (info == null && Type.getArgumentCount(methodNode.desc) == 3) { + info = extractPolymorphicDecryptor(classWrapper, methodNode).orElse(null); + } + + if (info != null) { + decryptors.put(info.methodRef(), info); + } + } + } + + return decryptors; + } + + private Optional findClass(Collection classes, String name) { + for (ClassWrapper classWrapper : classes) { + if (classWrapper.name().equals(name)) return Optional.of(classWrapper); + } + return Optional.empty(); + } + + private Optional extractDefaultDecryptor(ClassWrapper classWrapper, MethodNode methodNode) { + FieldRef stringField = firstGetStatic(methodNode, "[Ljava/lang/String;").orElse(null); + FieldRef cacheField = firstGetStatic(methodNode, "[Ljava/lang/Object;").orElse(null); + if (stringField == null || cacheField == null || !hasStackTraceGuard(methodNode)) return Optional.empty(); + + XorLocal idx = findIndexedXor(methodNode).orElse(null); + XorLocal trace = findTraceXor(methodNode).orElse(null); + if (idx == null || trace == null) return Optional.empty(); + + int[] keys = Arrays.stream(methodNode.instructions.toArray()) + .filter(TableSwitchInsnNode.class::isInstance) + .map(TableSwitchInsnNode.class::cast) + .filter(tableSwitch -> tableSwitch.labels.size() == 32) + .map(this::extractTableKeys) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + if (keys == null) return Optional.empty(); + + return Optional.of(new DefaultDecryptorInfo( + MethodRef.of(classWrapper.classNode(), methodNode), + stringField, + cacheField, + idx.constant, + trace.constant, + keys + )); + } + + private Optional extractPolymorphicDecryptor(ClassWrapper classWrapper, MethodNode methodNode) { + FieldRef stringField = firstGetStatic(methodNode, "[Ljava/lang/String;").orElse(null); + FieldRef cacheField = firstGetStatic(methodNode, "[Ljava/lang/Object;").orElse(null); + if (stringField == null || cacheField == null || !hasStackTraceGuard(methodNode)) return Optional.empty(); + + XorLocal idx = findIndexedXor(methodNode).orElse(null); + XorLocal trace = findTraceXor(methodNode).orElse(null); + if (idx == null || trace == null) return Optional.empty(); + + List argSlots = MethodHelper.argumentSlots(methodNode.access, methodNode.desc); + int indexArg = argSlots.indexOf(idx.local); + if (indexArg == -1) return Optional.empty(); + + AbstractInsnNode cast = Arrays.stream(methodNode.instructions.toArray()) + .filter(insn -> insn.getOpcode() == I2C) + .findFirst() + .orElse(null); + if (cast == null) return Optional.empty(); + + AbstractInsnNode finalXor = AsmHelper.previousMeaningful(cast); + AbstractInsnNode shift = AsmHelper.previousMeaningful(finalXor); + AbstractInsnNode shiftBy = AsmHelper.previousMeaningful(shift); + AbstractInsnNode key2Load = AsmHelper.previousMeaningful(shiftBy); + AbstractInsnNode key1Xor = AsmHelper.previousMeaningful(key2Load); + AbstractInsnNode key1Load = AsmHelper.previousMeaningful(key1Xor); + AbstractInsnNode hashXor = AsmHelper.previousMeaningful(key1Load); + AbstractInsnNode hashLoad = AsmHelper.previousMeaningful(hashXor); + + if (finalXor == null || finalXor.getOpcode() != IXOR || shift == null || shift.getOpcode() != ISHR) return Optional.empty(); + if (shiftBy == null || !shiftBy.isInteger() || shiftBy.asInteger() != 16) return Optional.empty(); + if (!(key2Load instanceof VarInsnNode key2Var) || key2Var.getOpcode() != ILOAD) return Optional.empty(); + if (key1Xor == null || key1Xor.getOpcode() != IXOR) return Optional.empty(); + if (!(key1Load instanceof VarInsnNode key1Var) || key1Var.getOpcode() != ILOAD) return Optional.empty(); + if (hashXor == null || hashXor.getOpcode() != IXOR) return Optional.empty(); + if (!(hashLoad instanceof VarInsnNode hashVar) || hashVar.getOpcode() != ILOAD || hashVar.var != trace.local) return Optional.empty(); + + int key1Arg = argSlots.indexOf(key1Var.var); + int key2Arg = argSlots.indexOf(key2Var.var); + if (key1Arg == -1 || key2Arg == -1) return Optional.empty(); + + List masks = extractPolymorphicMasks(methodNode, hashLoad); + if (masks.isEmpty()) return Optional.empty(); + + return Optional.of(new PolymorphicDecryptorInfo( + MethodRef.of(classWrapper.classNode(), methodNode), + stringField, + cacheField, + idx.constant, + trace.constant, + indexArg, + key1Arg, + key2Arg, + masks + )); + } + + private Optional extractStringPool( + ClassWrapper classWrapper, + FieldRef stringField, + Map fieldValues, + Map methodSalts + ) { + Optional simulated = simulateStringPoolInitializer(classWrapper, stringField, fieldValues); + if (simulated.isPresent()) return simulated; + + MethodNode clinit = classWrapper.findClInit().orElse(null); + if (clinit == null) return Optional.empty(); + + Map> frames; + try { + frames = MethodHelper.analyzeSource(classWrapper.classNode(), clinit); + } catch (RuntimeException ignored) { + return Optional.empty(); + } + + Map locals = MethodHelper.collectKnownIntLocals( + clinit, + frames, + fieldValues, + methodSalts.get(MethodRef.of(classWrapper.classNode(), clinit)) + ); + + for (AbstractInsnNode insn : clinit.instructions.toArray()) { + if (!(insn instanceof FieldInsnNode putStatic) || putStatic.getOpcode() != PUTSTATIC) continue; + if (!FieldRef.of(putStatic).equals(stringField)) continue; + + PoolInitializer initializer = findPoolInitializer(clinit, frames, locals, fieldValues, putStatic).orElse(null); + if (initializer == null) continue; + + if (initializer.xorKeys == null) { + String[] decoded = splitByLengths(initializer.data, initializer.lengths, initializer.key, false); + if (decoded != null) return Optional.of(decoded); + continue; + } + + String[] decoded = decodeXorInitializer(initializer, false); + if (decoded != null) return Optional.of(decoded); + + decoded = decodeXorInitializer(initializer, true); + if (decoded != null) return Optional.of(decoded); + } + + return Optional.empty(); + } + + private Optional simulateStringPoolInitializer( + ClassWrapper classWrapper, + FieldRef stringField, + Map fieldValues + ) { + MethodNode clinit = classWrapper.findClInit().orElse(null); + if (clinit == null) return Optional.empty(); + + return new StringPoolClinitInterpreter(classWrapper, clinit, stringField, fieldValues).run(); + } + + private Optional findPoolInitializer( + MethodNode clinit, + Map> frames, + Map locals, + Map fieldValues, + FieldInsnNode putStatic + ) { + AbstractInsnNode lenStringInsn = null; + AbstractInsnNode cursor = putStatic; + while ((cursor = cursor.getPrevious()) != null) { + if (cursor instanceof LdcInsnNode ldc + && ldc.cst instanceof String + && AsmHelper.nextMeaningful(cursor) instanceof MethodInsnNode call + && call.owner.equals("java/lang/String") + && call.name.equals("toCharArray") + && call.desc.equals("()[C")) { + lenStringInsn = cursor; + break; + } + } + if (!(lenStringInsn instanceof LdcInsnNode lenLdc)) return Optional.empty(); + + AbstractInsnNode dataStringInsn = lenStringInsn; + while ((dataStringInsn = dataStringInsn.getPrevious()) != null) { + if (dataStringInsn instanceof LdcInsnNode ldc && ldc.cst instanceof String) break; + } + if (!(dataStringInsn instanceof LdcInsnNode dataLdc)) return Optional.empty(); + + Integer key = null; + AbstractInsnNode keyStore = dataStringInsn; + while ((keyStore = keyStore.getPrevious()) != null) { + if (!(keyStore instanceof VarInsnNode store) || store.getOpcode() != ISTORE) continue; + + Frame frame = frames.get(store); + if (frame == null || frame.getStackSize() == 0) continue; + + Optional evaluated = MethodHelper.evaluateInt( + frame.getStack(frame.getStackSize() - 1), + frames, + locals, + fieldValues + ); + if (evaluated.isPresent()) { + key = evaluated.get(); + break; + } + } + if (key == null) return Optional.empty(); + + int[] xorKeys = null; + AbstractInsnNode switchCursor = lenStringInsn; + while ((switchCursor = switchCursor.getNext()) != null && switchCursor != putStatic) { + if (switchCursor instanceof TableSwitchInsnNode tableSwitch) { + xorKeys = extractTableKeys(tableSwitch); + break; + } + } + + return Optional.of(new PoolInitializer((String) dataLdc.cst, (String) lenLdc.cst, key, xorKeys)); + } + + private String[] decodeXorInitializer(PoolInitializer initializer, boolean twice) { + String[] chunks = splitByLengths(initializer.data, initializer.lengths, initializer.key, twice); + if (chunks == null) return null; + + String[] decoded = new String[chunks.length]; + for (int i = 0; i < chunks.length; i++) { + decoded[i] = xor(chunks[i], twice ? initializer.key : 0, initializer.xorKeys); + } + return decoded; + } + + private String xor(String text, int key, int[] keys) { + char[] chars = text.toCharArray(); + for (int i = 0; i < chars.length; i++) { + chars[i] = (char) (chars[i] ^ keys[i % keys.length] ^ key); + } + return new String(chars); + } + + private String[] splitByLengths(String data, String lengths, int key, boolean rawLengths) { + int[] decodedLengths = new int[lengths.length()]; + int total = 0; + for (int i = 0; i < lengths.length(); i++) { + int len = rawLengths ? lengths.charAt(i) : lengths.charAt(i) ^ key; + if (len < 0) return null; + decodedLengths[i] = len; + total += len; + } + if (total != data.length()) return null; + + String[] result = new String[decodedLengths.length]; + int offset = 0; + for (int i = 0; i < decodedLengths.length; i++) { + result[i] = data.substring(offset, offset + decodedLengths[i]); + offset += decodedLengths[i]; + } + return result; + } + + private Optional decrypt( + DecryptorInfo decryptor, + String[] pool, + ClassWrapper callerClass, + MethodNode callerMethod, + MethodInsnNode call, + Map> frames, + Map locals, + Map fieldValues + ) { + Frame frame = frames.get(call); + if (frame == null) return Optional.empty(); + + int argumentCount = Type.getArgumentCount(call.desc); + if (frame.getStackSize() < argumentCount) return Optional.empty(); + + int[] args = new int[argumentCount]; + for (int i = 0; i < argumentCount; i++) { + OriginalSourceValue source = frame.getStack(frame.getStackSize() - argumentCount + i); + Optional value = MethodHelper.evaluateInt(source, frames, locals, fieldValues); + if (value.isEmpty()) return Optional.empty(); + args[i] = value.get(); + } + + try { + if (decryptor instanceof DefaultDecryptorInfo info) { + return Optional.of(decryptDefault(info, pool, callerClass, callerMethod, args)); + } + if (decryptor instanceof PolymorphicDecryptorInfo info) { + return Optional.of(decryptPolymorphic(info, pool, callerClass, callerMethod, args)); + } + return Optional.empty(); + } catch (RuntimeException ignored) { + return Optional.empty(); + } + } + + private String decryptDefault(DefaultDecryptorInfo info, String[] pool, ClassWrapper callerClass, MethodNode callerMethod, int[] args) { + int index = args[0] ^ info.indexXor; + String encrypted = pool[index]; + int key = args[1] >> 16; + int traceKey = traceKey(callerClass, callerMethod, info.traceXor); + + char[] chars = encrypted.toCharArray(); + for (int i = 0; i < chars.length; i++) { + chars[i] = (char) (chars[i] ^ info.keys[i & (info.keys.length - 1)] ^ key ^ traceKey); + } + return new String(chars).intern(); + } + + private String decryptPolymorphic(PolymorphicDecryptorInfo info, String[] pool, ClassWrapper callerClass, MethodNode callerMethod, int[] args) { + int index = args[info.indexArg] ^ info.indexXor; + String encrypted = pool[index]; + int firstKey = args[info.key1Arg]; + int key = args[info.key2Arg] >> 16; + int traceKey = traceKey(callerClass, callerMethod, info.traceXor); + + char[] chars = encrypted.toCharArray(); + for (int i = 0; i < chars.length; i++) { + int value = chars[i]; + for (IntMask mask : info.masks) { + value = mask.apply(value); + } + chars[i] = (char) (value ^ traceKey ^ firstKey ^ key); + } + return new String(chars).intern(); + } + + private int traceKey(ClassWrapper callerClass, MethodNode callerMethod, int traceXor) { + return ((callerClass.canonicalName().hashCode() ^ callerMethod.name.hashCode()) >> 16) ^ traceXor; + } + + private void removeUnusedDecryptors(Collection classes, Set decryptors, Set stillUsed) { + if (decryptors.isEmpty()) return; + + Set referenced = new HashSet<>(stillUsed); + for (ClassWrapper classWrapper : classes) { + for (MethodNode methodNode : classWrapper.methods()) { + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (insn instanceof MethodInsnNode call) { + MethodRef ref = MethodRef.of(call); + if (decryptors.contains(ref)) referenced.add(ref); + } + } + } + } + + for (ClassWrapper classWrapper : classes) { + boolean removed = classWrapper.methods().removeIf(method -> { + MethodRef ref = MethodRef.of(classWrapper.classNode(), method); + return decryptors.contains(ref) && !referenced.contains(ref); + }); + if (removed) markChange(); + } + } + + private Optional firstGetStatic(MethodNode methodNode, String desc) { + return Arrays.stream(methodNode.instructions.toArray()) + .filter(FieldInsnNode.class::isInstance) + .map(FieldInsnNode.class::cast) + .filter(field -> field.getOpcode() == GETSTATIC && field.desc.equals(desc)) + .map(FieldRef::of) + .findFirst(); + } + + private boolean hasStackTraceGuard(MethodNode methodNode) { + boolean hasThrowable = false; + boolean hasClassName = false; + boolean hasMethodName = false; + boolean hasIntern = false; + + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (!(insn instanceof MethodInsnNode call)) continue; + hasThrowable |= call.owner.equals("java/lang/Throwable") && call.name.equals("getStackTrace"); + hasClassName |= call.owner.equals("java/lang/StackTraceElement") && call.name.equals("getClassName"); + hasMethodName |= call.owner.equals("java/lang/StackTraceElement") && call.name.equals("getMethodName"); + hasIntern |= call.owner.equals("java/lang/String") && call.name.equals("intern"); + } + + return hasThrowable && hasClassName && hasMethodName && hasIntern; + } + + private Optional findIndexedXor(MethodNode methodNode) { + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (insn.getOpcode() != IXOR) continue; + + AbstractInsnNode constant = AsmHelper.previousMeaningful(insn); + AbstractInsnNode load = AsmHelper.previousMeaningful(constant); + AbstractInsnNode store = AsmHelper.nextMeaningful(insn); + if (!(load instanceof VarInsnNode varInsn) || varInsn.getOpcode() != ILOAD) continue; + if (constant == null || !constant.isInteger()) continue; + if (!(store instanceof VarInsnNode storeVar) || storeVar.getOpcode() != ISTORE) continue; + + return Optional.of(new XorLocal(varInsn.var, storeVar.var, constant.asInteger())); + } + return Optional.empty(); + } + + private Optional findTraceXor(MethodNode methodNode) { + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (insn.getOpcode() != IXOR) continue; + + AbstractInsnNode constant = AsmHelper.previousMeaningful(insn); + AbstractInsnNode shift = AsmHelper.previousMeaningful(constant); + AbstractInsnNode store = AsmHelper.nextMeaningful(insn); + if (constant == null || !constant.isInteger()) continue; + if (shift == null || shift.getOpcode() != ISHR) continue; + if (!(store instanceof VarInsnNode storeVar) || storeVar.getOpcode() != ISTORE) continue; + + return Optional.of(new XorLocal(-1, storeVar.var, constant.asInteger())); + } + return Optional.empty(); + } + + private int[] extractTableKeys(TableSwitchInsnNode tableSwitch) { + if (tableSwitch.labels == null || tableSwitch.labels.isEmpty()) return null; + int[] keys = new int[tableSwitch.labels.size()]; + + for (int i = 0; i < tableSwitch.labels.size(); i++) { + AbstractInsnNode valueInsn = AsmHelper.nextMeaningful(tableSwitch.labels.get(i)); + if (valueInsn == null || !valueInsn.isInteger()) return null; + keys[i] = valueInsn.asInteger(); + } + + return keys; + } + + private List extractPolymorphicMasks(MethodNode methodNode, AbstractInsnNode stopAt) { + AbstractInsnNode caload = null; + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (insn.getOpcode() == CALOAD) { + caload = insn; + break; + } + } + if (caload == null) return List.of(); + + List masks = new ArrayList<>(); + AbstractInsnNode cursor = caload; + while ((cursor = AsmHelper.nextMeaningful(cursor)) != null && cursor != stopAt) { + int opcode = cursor.getOpcode(); + if (opcode != IADD && opcode != ISUB && opcode != IXOR) continue; + + AbstractInsnNode constant = AsmHelper.previousMeaningful(cursor); + if (constant == null || !constant.isInteger()) continue; + masks.add(new IntMask(opcode, constant.asInteger())); + } + + return masks; + } + + private sealed interface DecryptorInfo permits DefaultDecryptorInfo, PolymorphicDecryptorInfo { + MethodRef methodRef(); + FieldRef stringField(); + FieldRef cacheField(); + int indexXor(); + int traceXor(); + } + + private record DefaultDecryptorInfo( + MethodRef methodRef, + FieldRef stringField, + FieldRef cacheField, + int indexXor, + int traceXor, + int[] keys + ) implements DecryptorInfo { + } + + private record PolymorphicDecryptorInfo( + MethodRef methodRef, + FieldRef stringField, + FieldRef cacheField, + int indexXor, + int traceXor, + int indexArg, + int key1Arg, + int key2Arg, + List masks + ) implements DecryptorInfo { + } + + private record PoolInitializer(String data, String lengths, int key, int[] xorKeys) { + } + + private record XorLocal(int loadedLocal, int local, int constant) { + } + + private record IntMask(int opcode, int value) { + int apply(int input) { + return switch (opcode) { + case IADD -> input + value; + case ISUB -> input - value; + case IXOR -> input ^ value; + default -> input; + }; + } + } + + private static final class StringPoolClinitInterpreter { + private static final int MAX_STEPS = 100_000; + + private final ClassWrapper owner; + private final MethodNode method; + private final FieldRef stringField; + private final Map knownInts; + private final AbstractInsnNode[] instructions; + private final Map labels = new HashMap<>(); + private final Map statics = new HashMap<>(); + private final List stack = new ArrayList<>(); + private final Object[] locals; + + private int pc; + + private StringPoolClinitInterpreter( + ClassWrapper owner, + MethodNode method, + FieldRef stringField, + Map knownInts + ) { + this.owner = owner; + this.method = method; + this.stringField = stringField; + this.knownInts = knownInts; + this.instructions = method.instructions.toArray(); + this.locals = new Object[Math.max(method.maxLocals + 16, 64)]; + + for (int i = 0; i < instructions.length; i++) { + if (instructions[i] instanceof LabelNode label) labels.put(label, i); + } + knownInts.forEach(statics::put); + } + + private Optional run() { + try { + for (int steps = 0; pc >= 0 && pc < instructions.length && steps < MAX_STEPS; steps++) { + AbstractInsnNode insn = instructions[pc]; + pc++; + + if (insn instanceof LabelNode || insn.getType() == AbstractInsnNode.LINE || insn.getType() == AbstractInsnNode.FRAME) { + continue; + } + + String[] pool = execute(insn); + if (pool != null) return Optional.of(pool); + } + } catch (RuntimeException ignored) { + return Optional.empty(); + } + return Optional.empty(); + } + + private String[] execute(AbstractInsnNode insn) { + if (insn.isInteger()) { + push(insn.asInteger()); + return null; + } + + if (insn instanceof LdcInsnNode ldc) { + push(ldc.cst); + return null; + } + + if (insn instanceof VarInsnNode varInsn) { + executeVarInsn(varInsn); + return null; + } + + if (insn instanceof IincInsnNode iincInsn) { + locals[iincInsn.var] = asInt(locals[iincInsn.var]) + iincInsn.incr; + return null; + } + + if (insn instanceof JumpInsnNode jumpInsn) { + executeJump(jumpInsn); + return null; + } + + if (insn instanceof LookupSwitchInsnNode lookupSwitch) { + int key = popInt(); + int index = lookupSwitch.keys.indexOf(key); + jumpTo(index == -1 ? lookupSwitch.dflt : lookupSwitch.labels.get(index)); + return null; + } + + if (insn instanceof TableSwitchInsnNode tableSwitch) { + int key = popInt(); + if (key < tableSwitch.min || key > tableSwitch.max) { + jumpTo(tableSwitch.dflt); + } else { + jumpTo(tableSwitch.labels.get(key - tableSwitch.min)); + } + return null; + } + + if (insn instanceof TypeInsnNode typeInsn) { + executeTypeInsn(typeInsn); + return null; + } + + if (insn instanceof IntInsnNode intInsn) { + executeIntInsn(intInsn); + return null; + } + + if (insn instanceof FieldInsnNode fieldInsn) { + return executeFieldInsn(fieldInsn); + } + + if (insn instanceof MethodInsnNode methodInsn) { + executeMethodInsn(methodInsn); + return null; + } + + executeSimpleInsn(insn); + return null; + } + + private void executeVarInsn(VarInsnNode insn) { + switch (insn.getOpcode()) { + case ILOAD, ALOAD, LLOAD -> push(locals[insn.var]); + case ISTORE, ASTORE, LSTORE -> locals[insn.var] = pop(); + default -> throw new IllegalStateException(); + } + } + + private void executeJump(JumpInsnNode insn) { + boolean jump = switch (insn.getOpcode()) { + case GOTO -> true; + case IFEQ -> popInt() == 0; + case IFNE -> popInt() != 0; + case IFLT -> popInt() < 0; + case IFGE -> popInt() >= 0; + case IFGT -> popInt() > 0; + case IFLE -> popInt() <= 0; + case IFNULL -> pop() == null; + case IFNONNULL -> pop() != null; + case IF_ICMPEQ -> { + int right = popInt(); + int left = popInt(); + yield left == right; + } + case IF_ICMPNE -> { + int right = popInt(); + int left = popInt(); + yield left != right; + } + case IF_ICMPLT -> { + int right = popInt(); + int left = popInt(); + yield left < right; + } + case IF_ICMPGE -> { + int right = popInt(); + int left = popInt(); + yield left >= right; + } + case IF_ICMPGT -> { + int right = popInt(); + int left = popInt(); + yield left > right; + } + case IF_ICMPLE -> { + int right = popInt(); + int left = popInt(); + yield left <= right; + } + default -> throw new IllegalStateException(); + }; + + if (jump) jumpTo(insn.label); + } + + private void executeTypeInsn(TypeInsnNode insn) { + switch (insn.getOpcode()) { + case NEW -> push(new NewObject(insn.desc)); + case ANEWARRAY -> { + int length = popInt(); + if (insn.desc.equals("java/lang/String")) { + push(new String[length]); + } else { + push(new Object[length]); + } + } + case CHECKCAST -> {} + default -> throw new IllegalStateException(); + } + } + + private void executeIntInsn(IntInsnNode insn) { + if (insn.getOpcode() == NEWARRAY) { + int length = popInt(); + switch (insn.operand) { + case T_INT -> push(new int[length]); + case T_BYTE -> push(new byte[length]); + case T_CHAR -> push(new char[length]); + default -> push(new Object[length]); + } + return; + } + push(insn.operand); + } + + private String[] executeFieldInsn(FieldInsnNode insn) { + FieldRef ref = FieldRef.of(insn); + switch (insn.getOpcode()) { + case GETSTATIC -> push(statics.get(ref)); + case PUTSTATIC -> { + Object value = pop(); + if (ref.equals(stringField) && value instanceof String[] strings) { + return strings; + } + statics.put(ref, value); + } + default -> throw new IllegalStateException(); + } + return null; + } + + private void executeMethodInsn(MethodInsnNode insn) { + Type[] argumentTypes = Type.getArgumentTypes(insn.desc); + Object[] args = new Object[argumentTypes.length]; + for (int i = argumentTypes.length - 1; i >= 0; i--) { + args[i] = pop(); + } + Object receiver = insn.getOpcode() == INVOKESTATIC ? null : pop(); + + Object result = invokeWhitelisted(insn, receiver, args); + if (Type.getReturnType(insn.desc).getSort() != Type.VOID) { + push(result); + } + } + + private Object invokeWhitelisted(MethodInsnNode insn, Object receiver, Object[] args) { + if (insn.owner.equals("java/lang/String") && insn.name.equals("") && insn.desc.equals("([C)V")) { + if (receiver instanceof NewObject object) object.value = new String((char[]) args[0]); + return null; + } + + if (insn.owner.equals("java/lang/String")) { + String str = asString(receiver); + return switch (insn.name + insn.desc) { + case "toCharArray()[C" -> str.toCharArray(); + case "substring(II)Ljava/lang/String;" -> str.substring(asInt(args[0]), asInt(args[1])); + case "intern()Ljava/lang/String;" -> str.intern(); + case "getBytes(Ljava/lang/String;)[B" -> str.getBytes(Charset.forName(asString(args[0]))); + case "hashCode()I" -> str.hashCode(); + default -> throw new IllegalStateException(); + }; + } + + if (insn.owner.equals("java/lang/Math") && insn.name.equals("floorMod") && insn.desc.equals("(II)I")) { + return Math.floorMod(asInt(args[0]), asInt(args[1])); + } + + if (insn.owner.equals("java/lang/invoke/MethodHandles") && insn.name.equals("lookup")) { + return new LookupToken(owner.canonicalName()); + } + + if (insn.owner.equals("java/lang/invoke/MethodHandles$Lookup") && insn.name.equals("lookupClass")) { + return new ClassToken(receiver instanceof LookupToken token ? token.className : owner.canonicalName()); + } + + if (insn.owner.equals("java/lang/Integer") && insn.name.equals("valueOf")) return args[0]; + if (insn.owner.equals("java/lang/Long") && insn.name.equals("valueOf")) return args[0]; + if (insn.owner.equals("java/lang/Integer") && insn.name.equals("intValue")) return asInt(receiver); + + if (Type.getReturnType(insn.desc).equals(Type.INT_TYPE)) { + FieldInsnNode nextPut = nextPutStatic(); + if (nextPut != null) { + Integer known = knownInts.get(FieldRef.of(nextPut)); + if (known != null) return known; + } + return 0; + } + + throw new IllegalStateException(); + } + + private FieldInsnNode nextPutStatic() { + for (int i = pc; i < instructions.length; i++) { + AbstractInsnNode next = instructions[i]; + if (next instanceof LabelNode || next.getType() == AbstractInsnNode.LINE || next.getType() == AbstractInsnNode.FRAME) { + continue; + } + return next instanceof FieldInsnNode field && field.getOpcode() == PUTSTATIC ? field : null; + } + return null; + } + + private void executeSimpleInsn(AbstractInsnNode insn) { + switch (insn.getOpcode()) { + case NOP -> {} + case ACONST_NULL -> push(null); + case POP -> pop(); + case DUP -> { + Object value = peek(); + push(value); + } + case DUP2 -> { + Object value2 = pop(); + Object value1 = pop(); + push(value1); + push(value2); + push(value1); + push(value2); + } + case SWAP -> { + Object value2 = pop(); + Object value1 = pop(); + push(value2); + push(value1); + } + case IADD -> push(popInt() + popInt()); + case ISUB -> { + int right = popInt(); + int left = popInt(); + push(left - right); + } + case IMUL -> push(popInt() * popInt()); + case IDIV -> { + int right = popInt(); + int left = popInt(); + push(left / right); + } + case IREM -> { + int right = popInt(); + int left = popInt(); + push(left % right); + } + case IXOR -> push(popInt() ^ popInt()); + case IAND -> push(popInt() & popInt()); + case IOR -> push(popInt() | popInt()); + case ISHL -> { + int right = popInt(); + int left = popInt(); + push(left << right); + } + case ISHR -> { + int right = popInt(); + int left = popInt(); + push(left >> right); + } + case IUSHR -> { + int right = popInt(); + int left = popInt(); + push(left >>> right); + } + case INEG -> push(-popInt()); + case I2L -> push((long) popInt()); + case I2C -> push((int) (char) popInt()); + case LXOR -> { + long right = asLong(pop()); + long left = asLong(pop()); + push(left ^ right); + } + case LMUL -> { + long right = asLong(pop()); + long left = asLong(pop()); + push(left * right); + } + case ARRAYLENGTH -> push(Array.getLength(pop())); + case CALOAD -> { + int index = popInt(); + char[] array = (char[]) pop(); + push((int) array[index]); + } + case BALOAD -> { + int index = popInt(); + byte[] array = (byte[]) pop(); + push((int) array[index]); + } + case IALOAD -> { + int index = popInt(); + int[] array = (int[]) pop(); + push(array[index]); + } + case AALOAD -> { + int index = popInt(); + Object array = pop(); + push(((Object[]) array)[index]); + } + case CASTORE -> { + int value = popInt(); + int index = popInt(); + char[] array = (char[]) pop(); + array[index] = (char) value; + } + case IASTORE -> { + int value = popInt(); + int index = popInt(); + int[] array = (int[]) pop(); + array[index] = value; + } + case AASTORE -> { + Object value = pop(); + int index = popInt(); + Object[] array = (Object[]) pop(); + array[index] = value; + } + case RETURN -> pc = instructions.length; + default -> throw new IllegalStateException(); + } + } + + private void jumpTo(LabelNode label) { + Integer index = labels.get(label); + if (index == null) throw new IllegalStateException(); + pc = index; + } + + private Object pop() { + if (stack.isEmpty()) throw new IllegalStateException(); + return stack.remove(stack.size() - 1); + } + + private int popInt() { + return asInt(pop()); + } + + private Object peek() { + if (stack.isEmpty()) throw new IllegalStateException(); + return stack.get(stack.size() - 1); + } + + private void push(Object value) { + stack.add(value); + } + + private static int asInt(Object value) { + if (value == null) return 0; + if (value instanceof Integer integer) return integer; + if (value instanceof Character character) return character; + if (value instanceof Byte byteValue) return byteValue; + if (value instanceof Short shortValue) return shortValue; + throw new IllegalStateException(); + } + + private static long asLong(Object value) { + if (value instanceof Long longValue) return longValue; + if (value instanceof Integer integer) return integer; + throw new IllegalStateException(); + } + + private static String asString(Object value) { + if (value instanceof String string) return string; + if (value instanceof NewObject object && object.value instanceof String string) return string; + throw new IllegalStateException(); + } + + private static final class NewObject { + private final String type; + private Object value; + + private NewObject(String type) { + this.type = type; + } + + @Override + public String toString() { + return value == null ? type : value.toString(); + } + } + + private record LookupToken(String className) { + } + + private record ClassToken(String className) { + } + } +}