diff --git a/CHANGELOG.MD b/CHANGELOG.MD index ee95ac9..1146418 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,12 +1,28 @@ # CHANGELOG -## 1.3.3 - -todo +## 1.4.0 + +支持将 `INVOKE*` 指令转为反射调用,结合其他配置可完成进阶混淆 + +```yaml +# 是否将 JVM INVOKE 指令改成反射调用 +# 注意:该功能会明显影响执行效率 +# 优点:经过该混淆后会更加难以分析 +# 缺点:该功能未经过完善测试不稳定 +enableReflect: false +# INVOKEVIRTUAL 转换 +enableReflectVirtual: false +# INVOKESTATIC 转换 +enableReflectStatic: false +# INVOKESPECIAL 转换 +enableReflectSpecial: false +# INVOKEINTERFACE 转换 +enableReflectInterface: false +``` 更新日志: -- todo +- [重要] 支持方法调用 `INVOKE` 指令改反射调用混淆 感谢以下用户的贡献: diff --git a/README.md b/README.md index 565adc7..803e227 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,10 @@ ![](img/006.png) +从 `1.4.0` 版本开始支持将 `INVOKE` 指令转为反射结合其他混淆方式隐藏特征 + +![](img/008.png) + ## 背景 `jar-analyzer` 系列曾有一款工具 `jar-obfuscator` 实现 `jar` 包的混淆 @@ -232,6 +236,63 @@ enableHideField: false # 可以防止大部分 IDEA 版本反编译 enableHideMethod: false +# 是否将 JVM INVOKE 指令改成反射调用 +# 注意:该功能会明显影响执行效率 +# 优点:经过该混淆后会更加难以分析 +# 缺点:该功能未经过完善测试不稳定 +enableReflect: false +# INVOKEVIRTUAL 转换 +enableReflectVirtual: false +# INVOKESTATIC 转换 +enableReflectStatic: false +# INVOKESPECIAL 转换 +enableReflectSpecial: false +# INVOKEINTERFACE 转换 +enableReflectInterface: false + +``` + +## test + +如何测试你混淆后的单个 `class` 可用? + +- 结合具体场景和项目测试,取决于实际情况 +- 覆盖到 `jar` 文件中测试,比较麻烦 +- 放到对应目录中使用 `java` 命令测试,更麻烦 +- 使用自定义 `ClassLoader` 测试,方便快速 + +```java +public class Test extends ClassLoader { + @Override + protected Class findClass(String name) throws ClassNotFoundException { + byte[] classData = getClassData(name); + if (classData == null) { + throw new ClassNotFoundException(name); + } + return defineClass(name, classData, 0, classData.length); + } + + private byte[] getClassData(String className) { + if ("test.ClassName".equals(className)) { + try { + // read bytes form obfuscated class + return Files.readAllBytes(Paths.get("Test_obf.class")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return null; + } + + public static void main(String[] args) throws Exception { + TestRunner loader = new TestRunner(); + Class clazz = loader.loadClass("test.ClassName"); + Object instance = clazz.getDeclaredConstructor().newInstance(); + // usually main method + Method method = clazz.getMethod("main", String[].class); + method.invoke(instance, new Object[]{args}); + } +} ``` ## Thanks diff --git a/img/008.png b/img/008.png new file mode 100644 index 0000000..9874d6b Binary files /dev/null and b/img/008.png differ diff --git a/src/main/java/me/n1ar4/clazz/obfuscator/asm/ReflectClassVisitor.java b/src/main/java/me/n1ar4/clazz/obfuscator/asm/ReflectClassVisitor.java new file mode 100644 index 0000000..8713e11 --- /dev/null +++ b/src/main/java/me/n1ar4/clazz/obfuscator/asm/ReflectClassVisitor.java @@ -0,0 +1,205 @@ +package me.n1ar4.clazz.obfuscator.asm; + +import me.n1ar4.clazz.obfuscator.Const; +import me.n1ar4.clazz.obfuscator.core.ObfEnv; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +public class ReflectClassVisitor extends ClassVisitor { + public ReflectClassVisitor(ClassVisitor classVisitor) { + super(Const.ASMVersion, classVisitor); + } + + @Override + public void visitSource(String source, String debug) { + } + + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); + return new MethodVisitor(Const.ASMVersion, mv) { + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + if (opcode == Opcodes.INVOKESPECIAL) { + if (!ObfEnv.config.isEnableReflectSpecial()) { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + return; + } + } + if (opcode == Opcodes.INVOKEVIRTUAL) { + if (!ObfEnv.config.isEnableReflectVirtual()) { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + return; + } + } + if (opcode == Opcodes.INVOKESTATIC) { + if (!ObfEnv.config.isEnableReflectStatic()) { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + return; + } + } + if (opcode == Opcodes.INVOKEINTERFACE) { + if (!ObfEnv.config.isEnableReflectInterface()) { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + return; + } + } + + if (opcode == Opcodes.INVOKEVIRTUAL || + opcode == Opcodes.INVOKESPECIAL || + opcode == Opcodes.INVOKESTATIC || + opcode == Opcodes.INVOKEINTERFACE) { + Type[] argumentTypes = Type.getArgumentTypes(descriptor); + int numParams = argumentTypes.length; + + mv.visitIntInsn(Opcodes.BIPUSH, numParams); + mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object"); + + int localVarIndex = (opcode == Opcodes.INVOKESTATIC) ? 0 : 1; + for (int i = 0; i < numParams; i++) { + mv.visitInsn(Opcodes.DUP); + mv.visitIntInsn(Opcodes.BIPUSH, i); + + Type argType = argumentTypes[i]; + switch (argType.getSort()) { + case Type.BOOLEAN: + mv.visitVarInsn(Opcodes.ILOAD, localVarIndex); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Boolean", + "valueOf", "(Z)Ljava/lang/Boolean;", false); + break; + case Type.CHAR: + mv.visitVarInsn(Opcodes.ILOAD, localVarIndex); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Character", + "valueOf", "(C)Ljava/lang/Character;", false); + break; + case Type.BYTE: + mv.visitVarInsn(Opcodes.ILOAD, localVarIndex); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Byte", + "valueOf", "(B)Ljava/lang/Byte;", false); + break; + case Type.SHORT: + mv.visitVarInsn(Opcodes.ILOAD, localVarIndex); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Short", + "valueOf", "(S)Ljava/lang/Short;", false); + break; + case Type.INT: + mv.visitVarInsn(Opcodes.ILOAD, localVarIndex); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Integer", + "valueOf", "(I)Ljava/lang/Integer;", false); + break; + case Type.FLOAT: + mv.visitVarInsn(Opcodes.FLOAD, localVarIndex); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Float", + "valueOf", "(F)Ljava/lang/Float;", false); + break; + case Type.LONG: + mv.visitVarInsn(Opcodes.LLOAD, localVarIndex); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Long", + "valueOf", "(J)Ljava/lang/Long;", false); + localVarIndex++; + break; + case Type.DOUBLE: + mv.visitVarInsn(Opcodes.DLOAD, localVarIndex); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Double", + "valueOf", "(D)Ljava/lang/Double;", false); + localVarIndex++; + break; + default: + mv.visitVarInsn(Opcodes.ALOAD, localVarIndex); + } + + mv.visitInsn(Opcodes.AASTORE); + localVarIndex++; + } + + String internalNameToClassName = owner.replace('/', '.'); + mv.visitLdcInsn(internalNameToClassName); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Class", "forName", + "(Ljava/lang/String;)Ljava/lang/Class;", false); + mv.visitLdcInsn(name); + + mv.visitIntInsn(Opcodes.BIPUSH, argumentTypes.length); + mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Class"); + for (int i = 0; i < argumentTypes.length; i++) { + mv.visitInsn(Opcodes.DUP); + mv.visitIntInsn(Opcodes.BIPUSH, i); + mv.visitLdcInsn(Type.getType(argumentTypes[i].getDescriptor())); + mv.visitInsn(Opcodes.AASTORE); + } + + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getMethod", + "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;", false); + + if (opcode == Opcodes.INVOKESTATIC) { + mv.visitInsn(Opcodes.ACONST_NULL); + } else { + mv.visitVarInsn(Opcodes.ALOAD, 0); + } + + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/reflect/Method", "invoke", + "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", false); + + Type returnType = Type.getReturnType(descriptor); + if (returnType.getSort() != Type.VOID) { + handleReturnType(returnType); + } else { + mv.visitInsn(Opcodes.POP); + } + + return; + } + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + + private void handleReturnType(Type returnType) { + String wrapperType; + String unwrapMethod; + + switch (returnType.getSort()) { + case Type.BOOLEAN: + wrapperType = "java/lang/Boolean"; + unwrapMethod = "booleanValue"; + break; + case Type.CHAR: + wrapperType = "java/lang/Character"; + unwrapMethod = "charValue"; + break; + case Type.BYTE: + wrapperType = "java/lang/Byte"; + unwrapMethod = "byteValue"; + break; + case Type.SHORT: + wrapperType = "java/lang/Short"; + unwrapMethod = "shortValue"; + break; + case Type.INT: + wrapperType = "java/lang/Integer"; + unwrapMethod = "intValue"; + break; + case Type.FLOAT: + wrapperType = "java/lang/Float"; + unwrapMethod = "floatValue"; + break; + case Type.LONG: + wrapperType = "java/lang/Long"; + unwrapMethod = "longValue"; + break; + case Type.DOUBLE: + wrapperType = "java/lang/Double"; + unwrapMethod = "doubleValue"; + break; + default: + if (returnType.getSort() == Type.OBJECT || returnType.getSort() == Type.ARRAY) { + mv.visitTypeInsn(Opcodes.CHECKCAST, returnType.getInternalName()); + } + return; + } + + mv.visitTypeInsn(Opcodes.CHECKCAST, wrapperType); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, wrapperType, unwrapMethod, + "()" + returnType.getDescriptor(), false); + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/me/n1ar4/clazz/obfuscator/config/BaseConfig.java b/src/main/java/me/n1ar4/clazz/obfuscator/config/BaseConfig.java index 5a9e753..977360b 100644 --- a/src/main/java/me/n1ar4/clazz/obfuscator/config/BaseConfig.java +++ b/src/main/java/me/n1ar4/clazz/obfuscator/config/BaseConfig.java @@ -30,6 +30,13 @@ public class BaseConfig { private boolean ignorePublic; + // beta: invoke -> reflect + private boolean enableReflect; + private boolean enableReflectVirtual; + private boolean enableReflectStatic; + private boolean enableReflectSpecial; + private boolean enableReflectInterface; + private String aesKey; private String aesDecName; private String aesKeyField; @@ -75,6 +82,12 @@ public boolean isValid() { System.out.println(ColorUtil.red("[ERROR] max junk must be between 1 and 10000")); return false; } + if (!enableReflect) { + if (enableReflectInterface || enableReflectVirtual || enableReflectStatic || enableReflectSpecial) { + System.out.println(ColorUtil.red("[ERROR] you must enable reflect first")); + return false; + } + } return true; } @@ -105,9 +118,16 @@ public static BaseConfig Default() { config.setObfuscateChars(new String[]{"i", "l", "L", "1", "I"}); config.setAdvanceStringName("iii"); config.setMethodBlackList(new String[]{"main"}); + // reflect 配置默认关闭 + config.setEnableReflect(false); + config.setEnableReflectInterface(false); + config.setEnableReflectSpecial(false); + config.setEnableReflectVirtual(false); + config.setEnableReflectStatic(false); return config; } + @SuppressWarnings("all") public void show() { System.out.println(ColorUtil.purple("[GLOBAL] Log Level -> ") + ColorUtil.green(logLevel)); @@ -151,6 +171,16 @@ public void show() { ColorUtil.green(String.valueOf(junkLevel))); System.out.println(ColorUtil.cyan("[Junk Obfuscate] Max Number in One Class -> ") + ColorUtil.green(String.valueOf(maxJunkOneClass))); + System.out.println(ColorUtil.yellow("Enable Reflection -> ") + + ColorUtil.green(String.valueOf(enableReflect))); + System.out.println(ColorUtil.cyan("[REFLECTION] Enable Reflection INVOKESPECIAL -> ") + + ColorUtil.green(String.valueOf(enableReflectSpecial))); + System.out.println(ColorUtil.cyan("[REFLECTION] Enable Reflection INVOKESTATIC -> ") + + ColorUtil.green(String.valueOf(enableReflectStatic))); + System.out.println(ColorUtil.cyan("[REFLECTION] Enable Reflection INVOKEVIRTUAL -> ") + + ColorUtil.green(String.valueOf(enableReflectVirtual))); + System.out.println(ColorUtil.cyan("[REFLECTION] Enable Reflection INVOKEINTERFACE -> ") + + ColorUtil.green(String.valueOf(enableReflectInterface))); } public boolean isQuiet() { @@ -328,4 +358,44 @@ public String[] getObfuscateChars() { public void setObfuscateChars(String[] obfuscateChars) { this.obfuscateChars = obfuscateChars; } + + public boolean isEnableReflect() { + return enableReflect; + } + + public void setEnableReflect(boolean enableReflect) { + this.enableReflect = enableReflect; + } + + public boolean isEnableReflectVirtual() { + return enableReflectVirtual; + } + + public void setEnableReflectVirtual(boolean enableReflectVirtual) { + this.enableReflectVirtual = enableReflectVirtual; + } + + public boolean isEnableReflectStatic() { + return enableReflectStatic; + } + + public void setEnableReflectStatic(boolean enableReflectStatic) { + this.enableReflectStatic = enableReflectStatic; + } + + public boolean isEnableReflectSpecial() { + return enableReflectSpecial; + } + + public void setEnableReflectSpecial(boolean enableReflectSpecial) { + this.enableReflectSpecial = enableReflectSpecial; + } + + public boolean isEnableReflectInterface() { + return enableReflectInterface; + } + + public void setEnableReflectInterface(boolean enableReflectInterface) { + this.enableReflectInterface = enableReflectInterface; + } } diff --git a/src/main/java/me/n1ar4/clazz/obfuscator/core/Runner.java b/src/main/java/me/n1ar4/clazz/obfuscator/core/Runner.java index deb3728..bfd9190 100644 --- a/src/main/java/me/n1ar4/clazz/obfuscator/core/Runner.java +++ b/src/main/java/me/n1ar4/clazz/obfuscator/core/Runner.java @@ -200,6 +200,12 @@ public static void run(Path path, BaseConfig config, boolean isApi, BaseCmd cmd) System.out.println( ColorUtil.blue("#################################################################")); } + + if (config.isEnableReflect()) { + ReflectTransformer.transform(); + logger.info("run reflect transformer finish"); + } + if (config.isEnableDeleteCompileInfo()) { DeleteInfoTransformer.transform(); logger.info("run delete info transformer finish"); diff --git a/src/main/java/me/n1ar4/clazz/obfuscator/transform/ReflectTransformer.java b/src/main/java/me/n1ar4/clazz/obfuscator/transform/ReflectTransformer.java new file mode 100644 index 0000000..d79ceb3 --- /dev/null +++ b/src/main/java/me/n1ar4/clazz/obfuscator/transform/ReflectTransformer.java @@ -0,0 +1,35 @@ +package me.n1ar4.clazz.obfuscator.transform; + +import me.n1ar4.clazz.obfuscator.Const; +import me.n1ar4.clazz.obfuscator.asm.ReflectClassVisitor; +import me.n1ar4.clazz.obfuscator.core.ObfEnv; +import me.n1ar4.log.LogManager; +import me.n1ar4.log.Logger; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; + +import java.nio.file.Files; +import java.nio.file.Path; + +public class ReflectTransformer { + private static final Logger logger = LogManager.getLogger(); + + public static void transform() { + Path classPath = Const.TEMP_PATH; + if (!Files.exists(classPath)) { + logger.error("class not exist: {}", classPath.toString()); + return; + } + try { + ClassReader classReader = new ClassReader(Files.readAllBytes(classPath)); + ClassWriter classWriter = new ClassWriter(classReader, + ObfEnv.config.isAsmAutoCompute() ? Const.WriterASMOptions : 0); + ReflectClassVisitor changer = new ReflectClassVisitor(classWriter); + classReader.accept(changer, Const.ReaderASMOptions); + Files.delete(classPath); + Files.write(classPath, classWriter.toByteArray()); + } catch (Exception ex) { + logger.error("transform error: {}", ex.toString()); + } + } +} diff --git a/src/main/resources/config.yaml b/src/main/resources/config.yaml index 738e450..969c8a7 100644 --- a/src/main/resources/config.yaml +++ b/src/main/resources/config.yaml @@ -65,3 +65,17 @@ enableHideField: false # 是否开启方法隐藏 # 可以防止大部分 IDEA 版本反编译 enableHideMethod: false + +# 是否将 JVM INVOKE 指令改成反射调用 +# 注意:该功能会明显影响执行效率 +# 优点:经过该混淆后会更加难以分析 +# 缺点:该功能未经过完善测试不稳定 +enableReflect: false +# INVOKEVIRTUAL 转换 +enableReflectVirtual: false +# INVOKESTATIC 转换 +enableReflectStatic: false +# INVOKESPECIAL 转换 +enableReflectSpecial: false +# INVOKEINTERFACE 转换 +enableReflectInterface: false \ No newline at end of file diff --git a/src/main/test/me/n1ar4/test/TestRunner.java b/src/main/test/me/n1ar4/test/TestRunner.java new file mode 100644 index 0000000..a2bf7d8 --- /dev/null +++ b/src/main/test/me/n1ar4/test/TestRunner.java @@ -0,0 +1,36 @@ +package me.n1ar4.test; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class TestRunner extends ClassLoader { + @Override + protected Class findClass(String name) throws ClassNotFoundException { + byte[] classData = getClassData(name); + if (classData == null) { + throw new ClassNotFoundException(name); + } + return defineClass(name, classData, 0, classData.length); + } + + private byte[] getClassData(String className) { + if ("me.n1ar4.test.Test".equals(className)) { + try { + return Files.readAllBytes(Paths.get("Test_obf.class")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return null; + } + + public static void main(String[] args) throws Exception { + TestRunner loader = new TestRunner(); + Class clazz = loader.loadClass("me.n1ar4.test.Test"); + Object instance = clazz.getDeclaredConstructor().newInstance(); + Method method = clazz.getMethod("main", String[].class); + method.invoke(instance, new Object[]{args}); + } +}