标签 ZEVMS 下的文章

ZEVMS 冒险岛服务端V79 ver2 破解服务端登录人数限制(3)Java Agent动态修改静态变量

上次我们通过X64dbg 拿到了运行脚本,本次我们使用Java Agent做进一步分析

  1. 试着dump 字节码,能否将解密后的字节码拿到
  2. 分析程序运行过程,试着动态修改变量以达成目的

在开始之前,大体介绍一下Java Agent,在java程序启动前或启动中可以对字节码进行修改。非常多框架和特性用到了java agent,比如 AspectJ 通过使用java agent织入字节码增强实现AOP,一些RPC框架使用java agent来做一些session id统一侵入等。参考文档:https://blog.csdn.net/ancinsdn/article/details/58276945

要加载java agent 当然,启动脚本也要做相应的修改:

java -javaagent:F:\workspace\class-dump-agent\target\dump-agent-1.0-SNAPSHOT.jar -server -Dnet.sf.odinms.wzpath=wz gui.ZEVMS

其中F:\workspace\class-dump-agent\target\dump-agent-1.0-SNAPSHOT.jar为java agent package后的jar包

1.dump 字节码

在类加载后,可以尝试将字节码存到文件中,逻辑很简单

public class ClassDumpAgent implements ClassFileTransformer {

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

        String filePath = "/tmp/" + className.replace('.', '/') + ".class";
        System.out.println("dump "+className + " to "+filePath + " size="+classfileBuffer.length);
        try {
            File file = new File(filePath);
            File fileParent = file.getParentFile();
            if(!fileParent.exists()){
                fileParent.mkdirs();
            }
            OutputStream out = new FileOutputStream(filePath);
            InputStream is = new ByteArrayInputStream(classfileBuffer);
            byte[] buff = new byte[1024];
            int len = 0;
            while((len=is.read(buff))!=-1){
                out.write(buff, 0, len);
            }
            is.close();
            out.close();
            System.out.println("dump "+ className + " complete");
        } catch (Exception e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        }
        return null;
    }
}

但结果不理想,dump出来的字节码依旧还是加密过的。
究其原因,是该加密的class文件是jvm里的C++书写的BootStrap ClassLoader解密和加载的,而不是在java层自己写的ClassLoader负责解密的,java agent拿到的依旧是输入到jvm之前的字节码。
可以说该JVM中,class文件的格式本就是加密后的方式,相当于jvm输入字节码的协议都修改了。

2.实时修改变量

虽然字节码反编译不好做,但是加载之后的类还是要遵循java内存模型的,所以我们可以通过反射拿到这些类的方法、成员变量,看看有什么办法。

public class ClassDumpAgent implements ClassFileTransformer {

    static Set classNames = new HashSet(){{
        this.add("constants.ServerConstants");
        this.add("gui.ZEVMS");
        this.add("server.ServerProperties");
        this.add("handling.world.MapleParty");
    }};
    public byte[] transform(ClassLoader loader, final String className, final Class<?> classBeingRedefined, ProtectionDomain protectionDomain, final byte[] classfileBuffer) throws IllegalClassFormatException {

        String classNamePot = className.replace('/','.');
        if(!classNames.contains(classNamePot)){
            return null;
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                startDump(className,classBeingRedefined,classfileBuffer);
            }
        }).start();

        return null;
    }

    public void startDump(String className,Class<?> classBeingRedefined, byte[] classfileBuffer){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String classNamePot = className.replace('/','.');
        try {
            Class cc = Class.forName(classNamePot);
            Field[] fields = Class.forName(classNamePot).getDeclaredFields();
            Method[] methods = cc.getDeclaredMethods();
            System.out.println("CLASS: "+classNamePot);
            System.out.println("field-------");
            Object instance = null;
            for(Field field : fields){
                System.out.println(field.getName() + ":" +field.getType().getName()+ "static:" + Modifier.isStatic(field.getModifiers()));

                if(field.getName().equals("instance")){
                    field.setAccessible(true);
                    instance = field.get(Object.class);
                    System.out.println(instance);
                }
                if(field.getName().equals("显示人数")){
                    field.setAccessible(true);
                    Object f = field.get(instance);
                    System.out.println(f);
                }
                if(field.getName().equals("props")){
                    field.setAccessible(true);
                    Object f = field.get(Object.class);
                    Properties props = (Properties)f;
                    props.list(System.out);
                }
            }
            System.out.println("method-------");
            for(Method method: methods){
                System.out.println(method.getName());
            }
            System.out.println("dump "+ className + " complete");

        } catch (Exception e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        }
    }
}

这样就可以将一些感兴趣的类静态变量内容打印出来看一下了

javaagent1

接下来这就是个纯体力和解密的活了,最终我找到了这个变量, handling.world.MapleParty 类中的静态变量"容纳人数"(PS:这个老哥是真的都起中文变量名呀)。

只消将这个变量设置大一些,就可以了。
实际运行时发现该变量不是程序启动后就设置的,而是有一个延时,所以程序里做了个循环设置的操作

            Class cc = Class.forName(classNamePot);
            Field[] fields = Class.forName(classNamePot).getDeclaredFields();
            Method[] methods = cc.getDeclaredMethods();
            Object instance = null;
            for(Field field : fields){
                if(Modifier.isStatic(field.getModifiers())){
                    field.setAccessible(true);
                    Object f = field.get(Object.class);
                    System.out.println("class="+classNamePot+" field=" + field.getName()+
                                ":" +field.getType().getName()+ " static:" + " value=" + f);

                }
                if(field.getName().equals("容纳人数")){
                    field.setAccessible(true);

                    field.set(Object.class,MAX_USERS);
                    final Field fl = field;
                    instance = field.get(Object.class);
                    System.out.println(field.getName() +" value="+instance);
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                int trys=6;
                                while (trys>0) {
                                    Thread.sleep(10000);
                                    Object f = fl.get(Object.class);
                                    System.out.println(fl.getName() + " value=" + f);
                                    fl.set(Object.class,MAX_USERS);
                                    System.out.println(fl.getName() + " value=" + MAX_USERS);
                                    trys--;
                                }
                            } catch (Exception e) {
                                e.printStackTrace();
                            }

                        }
                    }).start();
                }
            }

至此,通过外挂一个java agent 程序来动态修改程序参数的程序,最终实现了解除服务端不允许超过一个用户登录的限制,虽然粗糙但是管用。

ZEVMS 冒险岛服务端V79 ver2 破解服务端登录人数限制(2)x64dbg找到程序入口

上次说到ZEVMS这个服务端有可能使用了修改版本的JVM,导致无法dump内存、打印线程,也无法直接从外部把字节码暴露出来,那我们这次继续分析下这个程序是如何运行的,从原理着手考虑有什么办法。

整个服务端的入口是“启动服务端.exe”。是个exe文件,但是显然最终是运行了个java程序,所以我们实际看看这个程序内部是怎么运行的吧。

上x64dbg(这个程序OllyDbg无可奈何,x64dbg有效)

首先加载的是ntdll,我们直接“run to user code”走到“启动服务端.exe”的入口
x64dbg1

可以看到这个程序很短,单步几次后,在栈上看到了调用的具体bat文件:000000000014FE28 00000000006A9D10 L"C:\\Users\\zhong\\AppData\\Local\\Temp\\6084.tmp\\6085.tmp\\6086.bat"

x64dbg2

资源管理器上找到相应路径,找到该文件

@shift /0
@echo off
color 1A
set PATH=jdk\jre\bin
set JRE_HOME=jdk\jre
set JAVA_HOME=jdk\jre\bin
set CLASSPATH=.;dist\*
java -server -Dnet.sf.odinms.wzpath=wz gui.ZEVMS
pause

可以看到它使用的确实是程序自带的jdk,而且是server版本的,我们将这个bat文件放到游戏服务端的根目录,直接运行该文件,服务端也可以正常启动。

至此,我们找到实际使用的jvm和实际的main函数入口"gui.ZEVMS"接下来可以用更多的手段来分析这个程序了,后续我考虑使用java agent技术发掘更多的信息达成目的。

ZEVMS 冒险岛服务端V79 ver2 破解服务端登录人数限制(1)分析、尝试反编译

去年想追忆一下网游冒险岛,作为一个老年人肯定是玩v079版本,找到一个服务端zevms功能不错,但是服务端只能一个人登录,作为技术宅我想方设法绕过了限制,记录一下。
声明,该版本是2018年底的版本,现在已不能使用,新版的zevms v79服务端限制已经扩大为6人,无需破解,为了避免侵害zev的利益,所以现在才发布此方法。

1. 目录结构和反编译

该服务端和其他服务端一样,是MySQL作为数据库,Java作为服务端的,数据库的用户名密码是提供的,无需多费心思,大概查了一下没有限制人数的配置,所以尝试分析下java服务端的代码

解压dist目录下的ZEVMS.jar,得到一堆class文件,这个作者做了很多修改工作,提供了很多特性,是个很勤劳的服主。很奇妙的是他喜欢用中文作为类名、方法名和变量名,或许我应该拿他的示例去回答“为什么很少有人用中文编程?”这个知乎热帖

zevms.jar

尝试使用JD-GUI对.class文件进行反编译。但是失败了。难道是加密了?为了一探究竟,用WINHEX工具打开其中一个.class文件瞧瞧,开头四个十六进制是0xC1DEBEDE,而不是我们熟悉的0xCAFEBABE,而且文件头下部也是乱码,看来是加密了

winhex1

加密了的.class文件

winhex2

对照个正常的.class文件

我们知道,很多java程序为了防止破解,会自己写classLoader保护代码,读入文件后再解密为正常的字节码,但这种方式可以将内存中解密后的字节码dump出来就是原文了,说做就做,尝试jstak、jmap attach到这个进程上先看看情况。

$ ./jmap.exe 8696
Attaching to process ID 8696, please wait...
Error attaching to process: Windbg Error: WaitForEvent failed!
sun.jvm.hotspot.debugger.DebuggerException: Windbg Error: WaitForEvent failed!
        at sun.jvm.hotspot.debugger.windbg.WindbgDebuggerLocal.attach0(Native Method)
        at sun.jvm.hotspot.debugger.windbg.WindbgDebuggerLocal.attach(WindbgDebuggerLocal.java:152)
        at sun.jvm.hotspot.HotSpotAgent.attachDebugger(HotSpotAgent.java:671)
        at sun.jvm.hotspot.HotSpotAgent.setupDebuggerWin32(HotSpotAgent.java:569)
        at sun.jvm.hotspot.HotSpotAgent.setupDebugger(HotSpotAgent.java:335)
        at sun.jvm.hotspot.HotSpotAgent.go(HotSpotAgent.java:304)
        at sun.jvm.hotspot.HotSpotAgent.attach(HotSpotAgent.java:140)
        at sun.jvm.hotspot.tools.Tool.start(Tool.java:185)
        at sun.jvm.hotspot.tools.Tool.execute(Tool.java:118)
        at sun.jvm.hotspot.tools.PMap.main(PMap.java:72)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at sun.tools.jmap.JMap.runTool(JMap.java:201)
        at sun.tools.jmap.JMap.main(JMap.java:130)
$ ./jstack 8696
8696: ▒ܾ▒▒▒▒ʡ▒

看样子不行呢,有可能这个程序自己修改了虚拟机,不允许用这些探查工具。但是我是不会气馁的,预知后事如何,且看下回分解。