diff --git a/config.yml b/config.yml index 951cb8e..24a887e 100644 --- a/config.yml +++ b/config.yml @@ -12,6 +12,12 @@ arrayObfuscationEnabled: true # Obfuscate boolean constants (true/false -> value hidden with XOR) booleanObfuscationEnabled: true +# Control flow obfuscation (flatten method control flow via switch dispatcher) +flowObfuscationEnabled: true + +# Obfuscate string literals +stringObfuscationEnabled: true + # Class name generation classNamesRandom: true # true = random names per build, false = sequential (a, b, c...) classNameLength: 6 # minimum length of class names (1-32). Longer = harder to read. diff --git a/src/main/java/st3ix/obfuscator/transform/FlowObfuscator.java b/src/main/java/st3ix/obfuscator/transform/FlowObfuscator.java index ad22700..73bf2f1 100644 --- a/src/main/java/st3ix/obfuscator/transform/FlowObfuscator.java +++ b/src/main/java/st3ix/obfuscator/transform/FlowObfuscator.java @@ -77,6 +77,7 @@ public byte[] transform(byte[] classBytes) { private void transformMethod(String className, MethodNode method) { if ("".equals(method.name) || "".equals(method.name)) return; + if ("main".equals(method.name) && "([Ljava/lang/String;)V".equals(method.desc)) return; // skip main - frame merge issues if ((method.access & (Opcodes.ACC_NATIVE | Opcodes.ACC_ABSTRACT)) != 0) return; if (method.tryCatchBlocks != null && !method.tryCatchBlocks.isEmpty()) return; if (method.instructions == null || method.instructions.size() < MIN_INSTRUCTIONS) { @@ -122,18 +123,24 @@ private void transformMethod(String className, MethodNode method) { emitConst(newInsns, firstObfState); newInsns.add(new VarInsnNode(Opcodes.ISTORE, stateVar)); + // GOTO block 0 directly - never fall through to dispatcher. This avoids merging + // "uninitialized" locals (from init) with "int" (from block exits) at the dispatcher, + // which would produce "Top" and cause VerifyError on ILOAD of those locals. + newInsns.add(new JumpInsnNode(Opcodes.GOTO, blockLabelNodes.get(0))); + newInsns.add(dispatcherLabelNode); newInsns.add(new VarInsnNode(Opcodes.ILOAD, stateVar)); newInsns.add(new LdcInsnNode(key)); newInsns.add(new InsnNode(Opcodes.IXOR)); - org.objectweb.asm.tree.LabelNode[] switchLabels = new org.objectweb.asm.tree.LabelNode[blocks.size() + 1]; - for (int i = 0; i < blocks.size(); i++) { - switchLabels[i] = blockLabelNodes.get(i); + // Dispatcher is only reached from block exits (state 1..N-1). Index 0 never reaches + // the dispatcher, so switch 1..N-1 -> block1..blockN-1, default -> exit. + int n = blocks.size(); + org.objectweb.asm.tree.LabelNode[] switchLabels = new org.objectweb.asm.tree.LabelNode[n - 1]; + for (int i = 0; i < n - 1; i++) { + switchLabels[i] = blockLabelNodes.get(i + 1); } - switchLabels[blocks.size()] = exitLabelNode; - - newInsns.add(new TableSwitchInsnNode(0, blocks.size(), exitLabelNode, switchLabels)); + newInsns.add(new TableSwitchInsnNode(1, n - 1, exitLabelNode, switchLabels)); for (int i = 0; i < blocks.size(); i++) { BasicBlock block = blocks.get(i); @@ -159,7 +166,7 @@ private void transformMethod(String className, MethodNode method) { } newInsns.add(exitLabelNode); - newInsns.add(new InsnNode(Opcodes.RETURN)); + emitExitForReturnType(newInsns, method.desc); method.instructions.clear(); // Transfer node-by-node to avoid NPE in InsnList.add(InsnList) with complex structures @@ -185,6 +192,43 @@ private boolean returns(BasicBlock block) { || op == Opcodes.ATHROW; } + /** Emit dead-code exit (default switch target) with correct return type for verifier. */ + private void emitExitForReturnType(InsnList list, String methodDesc) { + Type ret = Type.getReturnType(methodDesc); + switch (ret.getSort()) { + case Type.VOID: + list.add(new InsnNode(Opcodes.RETURN)); + break; + case Type.INT: + case Type.BOOLEAN: + case Type.BYTE: + case Type.CHAR: + case Type.SHORT: + list.add(new InsnNode(Opcodes.ICONST_0)); + list.add(new InsnNode(Opcodes.IRETURN)); + break; + case Type.LONG: + list.add(new InsnNode(Opcodes.LCONST_0)); + list.add(new InsnNode(Opcodes.LRETURN)); + break; + case Type.FLOAT: + list.add(new InsnNode(Opcodes.FCONST_0)); + list.add(new InsnNode(Opcodes.FRETURN)); + break; + case Type.DOUBLE: + list.add(new InsnNode(Opcodes.DCONST_0)); + list.add(new InsnNode(Opcodes.DRETURN)); + break; + case Type.ARRAY: + case Type.OBJECT: + list.add(new InsnNode(Opcodes.ACONST_NULL)); + list.add(new InsnNode(Opcodes.ARETURN)); + break; + default: + list.add(new InsnNode(Opcodes.RETURN)); + } + } + private void emitConst(InsnList list, int value) { if (value >= -1 && value <= 5) { int op = value == -1 ? Opcodes.ICONST_M1 : @@ -325,10 +369,9 @@ private List buildBasicBlocks(MethodNode method) { } if (current != null) blocks.add(current); - // Linear methods (no branches) yield only 1 block - split artificially into chunks - if (blocks.size() == 1) { - blocks = splitLinearBlock(blocks.get(0)); - } + // Do NOT split linear methods: merging block exits at the dispatcher causes + // uninitialized+int -> Top and VerifyError. Linear methods stay as 1 block and are skipped. + // if (blocks.size() == 1) blocks = splitLinearBlock(blocks.get(0)); return blocks; } @@ -375,6 +418,7 @@ private static int stackDelta(AbstractInsnNode insn) { int op = insn.getOpcode(); switch (op) { case Opcodes.NOP: case Opcodes.GOTO: case Opcodes.RETURN: + case Opcodes.IINC: // no stack effect case Opcodes.IFEQ: case Opcodes.IFNE: case Opcodes.IFLT: case Opcodes.IFGE: case Opcodes.IFGT: case Opcodes.IFLE: case Opcodes.IF_ICMPEQ: case Opcodes.IF_ICMPNE: case Opcodes.IF_ICMPLT: case Opcodes.IF_ICMPGE: case Opcodes.IF_ICMPGT: case Opcodes.IF_ICMPLE: @@ -415,14 +459,29 @@ private static int stackDelta(AbstractInsnNode insn) { case Opcodes.AALOAD: case Opcodes.BALOAD: case Opcodes.CALOAD: case Opcodes.SALOAD: return -1; default: + if (op >= 26 && op <= 29) return 1; // ILOAD_0..3 + if (op >= 30 && op <= 33) return 2; // LLOAD_0..3 + if (op >= 34 && op <= 37) return 1; // FLOAD_0..3 + if (op >= 38 && op <= 41) return 2; // DLOAD_0..3 + if (op >= 42 && op <= 45) return 1; // ALOAD_0..3 + if (op >= 59 && op <= 62) return -1; // ISTORE_0..3 + if (op >= 63 && op <= 66) return -2; // LSTORE_0..3 + if (op >= 67 && op <= 70) return -1; // FSTORE_0..3 + if (op >= 71 && op <= 74) return -2; // DSTORE_0..3 + if (op >= 75 && op <= 78) return -1; // ASTORE_0..3 if (op >= Opcodes.INVOKEVIRTUAL && op <= Opcodes.INVOKEDYNAMIC && insn instanceof org.objectweb.asm.tree.MethodInsnNode mi) { int retSize = Type.getReturnType(mi.desc).getSize(); - int argSize = Type.getArgumentTypes(mi.desc).length; + int argSize = 0; + for (Type t : Type.getArgumentTypes(mi.desc)) argSize += t.getSize(); if (op != Opcodes.INVOKESTATIC && op != Opcodes.INVOKEDYNAMIC) argSize++; return retSize - argSize; } if (op == Opcodes.NEW || op == Opcodes.GETSTATIC || op == Opcodes.GETFIELD) return 1; if (op == Opcodes.PUTSTATIC || op == Opcodes.PUTFIELD) return -1; + if (op == Opcodes.NEWARRAY || op == Opcodes.ANEWARRAY) return 0; // pop 1, push 1 + if (op == Opcodes.MULTIANEWARRAY && insn instanceof org.objectweb.asm.tree.MultiANewArrayInsnNode ma) { + return 1 - ma.dims; // pop dims ints, push 1 + } return 0; } }