快速预览

更安全的临时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(署名)协议发布

更安全的临时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(署名)协议发布