如何修改Throwable.printStackTrace()实现MC服务端堆栈反混淆?
解决方案:两种可行的反混淆堆栈跟踪方法
既然你无法修改Minecraft服务器的底层代码,而且试过自定义Exception、重写Thread的getStackTrace()都无效(确实,getStackTrace()是JVM原生实现的,子类重写不会覆盖原生行为),我给你两个实用的方案,尽量减少外部依赖:
方案1:拦截System.err输出,实时替换混淆内容
这个方法利用printStackTrace()默认输出到System.err的特性,我们可以包装System.err,对输出的每一行堆栈信息进行反混淆替换。
实现步骤:
- 解析混淆映射:把你的混淆规则转换成两个Map——一个映射混淆类名到原类名,另一个映射
混淆类.混淆方法到原方法名。 - 自定义PrintStream:包装原始的
System.err,重写输出方法,识别堆栈行并替换内容。 - 启动前替换System.err:在调用服务器
main()方法之前,把自定义的PrintStream设置为新的System.err。
代码示例:
首先是自定义的反混淆输出流:
import java.io.PrintStream; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; public class DeobfuscatingPrintStream extends PrintStream { // 匹配堆栈行的正则:at 类名.方法名 (位置) private static final Pattern STACK_TRACE_PATTERN = Pattern.compile("at ([a-zA-Z0-9.$]+)(\\.([a-zA-Z0-9_]+))? \\((.*?)\\)"); private final Map<String, String> classMap; private final Map<String, String> methodMap; public DeobfuscatingPrintStream(PrintStream original, Map<String, String> classMap, Map<String, String> methodMap) { super(original); this.classMap = classMap; this.methodMap = methodMap; } @Override public void println(String line) { super.println(deobfuscateLine(line)); } @Override public void print(String line) { super.print(deobfuscateLine(line)); } private String deobfuscateLine(String line) { if (!line.startsWith("at ")) { return line; } Matcher matcher = STACK_TRACE_PATTERN.matcher(line); if (matcher.find()) { String obfClass = matcher.group(1); String obfMethod = matcher.group(3); String location = matcher.group(4); // 替换类名和方法名 String deobfClass = classMap.getOrDefault(obfClass, obfClass); String deobfMethod = obfMethod != null ? methodMap.getOrDefault(obfClass + "." + obfMethod, obfMethod) : null; // 重构堆栈行 StringBuilder sb = new StringBuilder("at ").append(deobfClass); if (deobfMethod != null) { sb.append(".").append(deobfMethod); } sb.append(" (").append(location).append(")"); return sb.toString(); } return line; } }
然后是初始化和启动代码:
import java.util.HashMap; import java.util.Map; public class YourServerLauncher { public static void main(String[] args) { // 1. 加载混淆映射(实际可以从文件读取,这里是示例) Map<String, String> classMapping = new HashMap<>(); Map<String, String> methodMapping = new HashMap<>(); classMapping.put("A", "underlying.cannot.be.modified.SomeClass"); methodMapping.put("A.a", "someMethod"); // Main类未混淆,无需添加 // 2. 替换System.err PrintStream originalErr = System.err; System.setErr(new DeobfuscatingPrintStream(originalErr, classMapping, methodMapping)); // 3. 启动Minecraft服务器 underlying.cannot.be.modified.Main.main(args); } }
这个方案的优势是零额外依赖,实现简单,完全符合你的需求——因为你可以控制服务器的启动流程。唯一的局限是只能拦截输出到System.err的堆栈,如果服务器有其他日志输出渠道,可能需要额外处理,但Minecraft vanilla服务器默认是输出到控制台的。
方案2:使用Java Agent+ASM修改Throwable的getStackTrace()
如果需要处理所有调用getStackTrace()的场景(比如服务器写入日志文件的堆栈),可以用Java Agent在类加载时修改Throwable的字节码,让它返回反混淆后的堆栈元素。
这个方案需要依赖轻量级的字节码操作库ASM(只有几十KB,几乎可以忽略),但能覆盖所有堆栈跟踪的生成场景。
实现步骤:
- 创建Java Agent:在
premain方法中注册类转换器,拦截Throwable类的加载。 - 修改Throwable的getStackTrace():用ASM插入代码,在原方法返回堆栈数组后,调用我们的工具方法进行反混淆。
- 打包Agent并启动服务器:将Agent打包成JAR,启动时用
-javaagent参数加载。
代码示例:
首先是Agent类:
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; public class DeobfuscationAgent { public static void premain(String agentArgs, Instrumentation inst) { inst.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { // 只处理Throwable类 if ("java/lang/Throwable".equals(className)) { ClassReader cr = new ClassReader(classfileBuffer); ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES); ClassVisitor cv = new ClassVisitor(Opcodes.ASM9, cw) { @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); // 重写getStackTrace方法 if ("getStackTrace".equals(name) && "()[Ljava/lang/StackTraceElement;".equals(desc)) { return new MethodVisitor(Opcodes.ASM9, mv) { @Override public void visitInsn(int opcode) { // 在原方法返回数组之前,调用我们的反混淆方法 if (opcode == Opcodes.ARETURN) { visitMethodInsn(Opcodes.INVOKESTATIC, "com/yourpackage/DeobfuscationUtils", "deobfuscate", "([Ljava/lang/StackTraceElement;)[Ljava/lang/StackTraceElement;", false); } super.visitInsn(opcode); } }; } return mv; } }; cr.accept(cv, ClassReader.EXPAND_FRAMES); return cw.toByteArray(); } return null; } }); } }
然后是反混淆工具类:
import java.util.Map; import java.util.HashMap; public class DeobfuscationUtils { private static final Map<String, String> CLASS_MAP = new HashMap<>(); private static final Map<String, String> METHOD_MAP = new HashMap<>(); static { // 初始化映射(实际从文件加载) CLASS_MAP.put("A", "underlying.cannot.be.modified.SomeClass"); METHOD_MAP.put("A.a", "someMethod"); } public static StackTraceElement[] deobfuscate(StackTraceElement[] original) { StackTraceElement[] deobfuscated = new StackTraceElement[original.length]; for (int i = 0; i < original.length; i++) { StackTraceElement elem = original[i]; String obfClass = elem.getClassName(); String obfMethod = elem.getMethodName(); String deobfClass = CLASS_MAP.getOrDefault(obfClass, obfClass); String deobfMethod = METHOD_MAP.getOrDefault(obfClass + "." + obfMethod, obfMethod); // 创建新的StackTraceElement替换原有的 deobfuscated[i] = new StackTraceElement( deobfClass, deobfMethod, elem.getFileName(), elem.getLineNumber() ); } return deobfuscated; } }
最后,打包Agent时需要在MANIFEST.MF中添加:
Premain-Class: com.yourpackage.DeobfuscationAgent Can-Redefine-Classes: true
启动服务器的命令:
java -javaagent:deobfuscation-agent.jar -jar server.jar
这个方案的优势是全面覆盖所有堆栈生成场景,不管是printStackTrace()还是其他获取堆栈的方式,都会被反混淆。缺点是需要额外打包Agent,但ASM依赖极小,几乎不会增加负担。
选择建议
- 如果你只需要处理控制台输出的堆栈,优先选方案1,简单快捷,零依赖。
- 如果需要处理日志文件等其他输出渠道的堆栈,选方案2,功能更全面。
内容的提问来源于stack exchange,提问作者Zevin Zenph Zambori

