Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
83 changes: 71 additions & 12 deletions src/main/java/st3ix/obfuscator/transform/FlowObfuscator.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public byte[] transform(byte[] classBytes) {

private void transformMethod(String className, MethodNode method) {
if ("<init>".equals(method.name) || "<clinit>".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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand All @@ -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 :
Expand Down Expand Up @@ -325,10 +369,9 @@ private List<BasicBlock> 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;
}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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;
}
}
Expand Down
Loading