美味的曲奇
快速预览


更安全的临时OP方案!by javaagent + javassist



  前言

        本帖起源于 【安全警告】停止使用setOp,performCommand和chat执行命令
        该贴中包含了对于 setOp 方法的两次 IO 导致的安全和性能问题的讨论
        而文章中的方案就是针对这方面的解决(缓解)思路

  警告 | 预防针

        1. 文章中的所有代码仅为证明可行性设计,代码中未添加任何额外的安全/鉴权措施,请注意
        2. 用命令来调用API是低效的,不被推荐的,但本次文章只讨论在这种情况下的关于优化setOp的措施 (缓 兵 之 计,毕竟用的人太多了)
        3. 文章中的代码测试于 1.20.2 版本,其他版本需要修改其中的类路径/方法名来进行兼容
        4. 打算将此逻辑应用于实际生产时请务必注意文末的已知缺陷
        5. 欢迎讨论/提出建议 :D

  实现逻辑

        通过 javaagent + javassist 为 isOp 判断添加额外的逻辑,当玩家的 UUID 被包含在一个无持久化的集合里时返回 true

  实现历程

        既然确定了要注入源码,当然可以修改Spigot层的 setOp 方法,但为了兼容原版命令就需要修改 NMS 下的最底层的 isOp 判定方式,于是我通过1.20.2 版本 json 中的反映射表定位了 isOp 逻辑的实现位置:net.minecraft.server.players.PlayerList#f(com.mojang.authlib.GameProfile)

        通过在其中插入堆栈输出得到了一份堆栈输出(无关部分入调度器已省略)
       java.lang.Exception
       at net.minecraft.server.players.PlayerList.f(PlayerList.java)
       at org.bukkit.craftbukkit.v1_20_R2.entity.CraftPlayer.isOp(CraftPlayer.java:256)
       at org.bukkit.permissions.PermissibleBase.isOp(PermissibleBase.java:36)
       at org.bukkit.permissions.PermissibleBase.hasPermission(PermissibleBase.java:84)
       at org.bukkit.craftbukkit.v1_20_R2.entity.CraftHumanEntity.hasPermission(CraftHumanEntity.java:233)
       at net.minecraft.commands.CommandListenerWrapper.hasPermission(CommandListenerWrapper.java:220)
       at net.minecraft.commands.CommandListenerWrapper.c(CommandListenerWrapper.java:205)
       at net.minecraft.server.commands.CommandSpreadPlayers.lambda$register$3(CommandSpreadPlayers.java:53)
       at com.mojang.brigadier.tree.CommandNode.canUse(CommandNode.java:79)
       at net.minecraft.commands.CommandDispatcher.a(CommandDispatcher.java:491)
       at net.minecraft.commands.CommandDispatcher.sendAsync(CommandDispatcher.java:443)
       at net.minecraft.commands.CommandDispatcher.lambda$sendCommands$5(CommandDispatcher.java:422)
可以看到,命令在关于Op方面的鉴权可以简化为
        
        这一步是为了确定准备修改的方法真的是目标方法,而不是同名

        具体实现便是在其中加入自己的代码和逻辑,目前实现的逻辑是在其中加入 ArrayList moskOp 的静态字段,在 f 方法中检查如果递入的 GameProfile.getId() 包含在其中
        

  代码参考
        无关代码/样板代码已省略,仅展示核心实现
        
        MockOpAgent/MockOpTransformer
        

package tech.cookiepower.mockop.agent;

import javassist.*;

import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class MockOpTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) {

        if (!className.equals("net/minecraft/server/players/PlayerList")) {
            return classfileBuffer;
        }

        CtClass classPlayerList = null;

        try {
            ClassPool pool = ClassPool.getDefault();
            classPlayerList = pool.makeClass(new ByteArrayInputStream(classfileBuffer));
            // add field
            CtField fieldMockOp = new CtField(pool.get("java.util.ArrayList"), "mockOp", classPlayerList);
            fieldMockOp.setModifiers(Modifier.PUBLIC);
            fieldMockOp.setModifiers(Modifier.STATIC);
            classPlayerList.addField(fieldMockOp,CtField.Initializer.byExpr("new java.util.ArrayList()"));
            // modify method
            CtMethod methodIsOp = classPlayerList.getMethod("f", "(Lcom/mojang/authlib/GameProfile;)Z");
            methodIsOp.insertBefore("if (mockOp.contains($1.getId())) { return true; }");
            System.out.println("MockOpTransformer Injected Successfully!");
            return classPlayerList.toBytecode();
        } catch (Exception e) {
            e.printStackTrace(System.out);
        }
        finally {
            if (classPlayerList != null) {
                classPlayerList.detach();
            }
        }
        return classfileBuffer;
    }
}



        MockOp/MockOpUtil


package tech.cookiepower.mockop;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.UUID;

public class MockOpUtil {
    private static ArrayListUUID> mockOpPoint;

    @SuppressWarnings("unchecked")
    public static void init() {
        try {
            Field fieldMockOp = Class.forName("net.minecraft.server.players.PlayerList")
                                .getDeclaredField("mockOp");
            fieldMockOp.setAccessible(true);
            mockOpPoint = (ArrayListUUID>) fieldMockOp.get(null);
        } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    public static void setMockOp(UUID uuid) {
        mockOpPoint.add(uuid);
    }

    public static void unsetMockOp(UUID uuid) {
        mockOpPoint.remove(uuid);
    }
}




  已知缺陷
        1. 因需要修改NMS代码而高度版本耦合
        2. 若每个需要op执行的插件均额外使用 javaagent 来添加逻辑会增加很多额外开销和冲突的可能

  下载演示
        





MockOp-1.0-SNAPSHOT-all.jar

(770.96 KB, 下载次数: 0)





        





MockOpAgent-1.0-SNAPSHOT-all.jar

(770.51 KB, 下载次数: 0)





        使用方法:
        1. MockOpAgent放在服务器根目录,MockOp放在插件目录
        2. 在启动服务器参数的 -jar 前添加 -javaagent:MockOpAgent-1.0-SNAPSHOT-all.jar
        3. 进入服务器使用 /mockop
  
本文通过 CC-BY(署名)协议发布

第一页 上一页 下一页 最后一页