You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修改Throwable.printStackTrace()实现MC服务端堆栈反混淆?

解决方案:两种可行的反混淆堆栈跟踪方法

既然你无法修改Minecraft服务器的底层代码,而且试过自定义Exception、重写Thread的getStackTrace()都无效(确实,getStackTrace()是JVM原生实现的,子类重写不会覆盖原生行为),我给你两个实用的方案,尽量减少外部依赖:


方案1:拦截System.err输出,实时替换混淆内容

这个方法利用printStackTrace()默认输出到System.err的特性,我们可以包装System.err,对输出的每一行堆栈信息进行反混淆替换。

实现步骤:
  1. 解析混淆映射:把你的混淆规则转换成两个Map——一个映射混淆类名到原类名,另一个映射混淆类.混淆方法到原方法名。
  2. 自定义PrintStream:包装原始的System.err,重写输出方法,识别堆栈行并替换内容。
  3. 启动前替换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,几乎可以忽略),但能覆盖所有堆栈跟踪的生成场景。

实现步骤:
  1. 创建Java Agent:在premain方法中注册类转换器,拦截Throwable类的加载。
  2. 修改Throwable的getStackTrace():用ASM插入代码,在原方法返回堆栈数组后,调用我们的工具方法进行反混淆。
  3. 打包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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:48:20