帖子

Memorial Edition

查看: 64|回复: 0

[插件开发教程] Write Up : MCG/DynaGuard:JVM层HIPS的原理与实现

[复制链接]

男同大王

人气
15 点
金粒
463 粒
宝石
0 颗
爱心
1 颗
钻石
79 颗
贡献
0 点
发表于 昨天 23:51 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
本帖最后由 huzpsb 于 2025-2-5 00:45 编辑

Write Up : MCG/DynaGuard:JVM层HIPS的原理与实现
MCG/DynaGuard模块对JVM的敏感行为进行探测与拦截的细节
【警告】
MCG是一个EOL项目,而本系列文章的公开无疑会再次降低MCG的安全性。无论如何,请不要再使用MCG
本系列文章旨在分享MCG的思路而不是源码;请不要尝试通过简单的复制粘贴来完成对MCG的重建。
本文涉及到大量Forge、JVM等包的无/少文档内部实现,其中有部分已经不适合最新版的实现。请自行查证最新版是否一致



▌Part 1 SecurityManager
1.1 SecurityManager的注册
我们知道,为JVM设置SecurityManager是非常简单的。我们只需要使用:
  1. System.setSecurityManager(sm);
复制代码
即可注册。这应该不会有什么问题...吧?
然后问题就来了。因为一个历史遗留问题,Forge会注册SecurityManager,并且cpw拒绝对此行为进行任何修改。
这将会带来一个问题,在Mohist与CatServer等混合核心上,当你尝试使用setSecurityManager时,会失败。(传送门
让我们想一下如何解决这个问题。首先,我们知道,SecurityManager是System的一个属性。
  1. private static volatile SecurityManager security = null;
复制代码
那么我们可不可以
  1. Field f = System.class.getDeclaredField("security");
  2. f.setAccessible(true);
  3. f.set(null, sm);
复制代码
答案是不行。因为f是null。为什么呢?其实是因为,getDeclaredField方法使用的是privateGetDeclaredFields,privateGetDeclaredFields中用了Reflection.filterFields;这会干扰我们的取得。
如果我们绕过它呢?getDeclaredFields0,启动?
  1. Method getDeclaredFields0M = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class);
  2. getDeclaredFields0M.setAccessible(true);
  3. Field[] fields = (Field[]) getDeclaredFields0M.invoke(System.class, false);
  4. Field securityField = null;
  5. for (Field field : fields) {
  6.     if (field.getName().equals("security")) {
  7.         securityField = field;
  8.         break;
  9.     }
  10. }
  11. securityField.setAccessible(true);
  12. securityField.set(null, mysm);
复制代码
getDeclaredFields0确实不在filterMethods的目录中(疑似是bug);而cpw并没有阻止这个反射操作。因此,我们可以用这个方法绕过Forge对SecurityManager的占用。
思考题:该如何避免我们的SecurityManager被使用相同方法替换?
答案:


1.2 SecurityManager的使用
其实我觉得这章不用写
在注册了SecurityManager后,我们就拥有了几乎全部的生杀夺予大权。举例而言,我们可以对命令执行进行拦截。
  1. @Override
  2. public void checkExec(String cmd) {
  3.     throw new SmException("Access denied (exec)");
  4. }
复制代码
当然,您可以判断cmd是否合理;过于简单,这里不再赘述。
类似的,我们可以限制文件读写、网络访问、包可见性等内容。



▌Part 2 URI注入
2.1 URI的注册
我们知道,一个典型的Java的联网代码的写法是:
  1. HttpURLConnection connection = (HttpURLConnection) new URL("http://example.com/?q=is-mcg-present").openConnection();
复制代码
这段代码的执行流程的关键步骤是:
  1. public URLConnection openConnection() throws java.io.IOException {
  2.     return handler.openConnection(this);
  3. }
复制代码
handler的定义是在URL的构造器中执行的。具体来说,URL.getURLStreamHandler会根据protocol返回对应的handler。
URL是经典的工厂模式,这是极好的。仅需自己实现InjectURI implements URLStreamHandlerFactory即可。
  1. Field field = URL.class.getDeclaredField("factory");
  2. field.setAccessible(true);
  3. URLStreamHandlerFactory factory = (URLStreamHandlerFactory) field.get(null);
  4. field.set(null, new InjectURI(factory));
复制代码
2.1 URI的使用
在InjectURI中覆写createURLStreamHandler即可完成对URI的拦截。
当然,利用SecurityManager就可以拦截插件的联网。从某种意义上说,sm比URI还安全,因为某些http客户端库可以绕过URI;另外,不使用http协议联网的方法也有很多。
但是URI插件可以实现两个更好的功能:
首先,对于SecurityManager来说,URI的具体内容是不透明的。换言之,例如,有一个https网址,你对它的ip以外一无所知。InjectURI允许了更精细的权限管理。
例如,可以维护一个状态标识符,允许InjectURI暂时性地为sm添加允许的ip。
另外,InjectURI允许对返回的内容进行修改。例如,考虑以下代码:
InjectedHandler:
  1. @Override
  2. protected URLConnection openConnection(URL u, Proxy p) throws IOException {
  3.     byte[] bytes = // Whatever
  4.     if (bytes != null) {
  5.         return new ModifiedCon(u, p, new ByteArrayInputStream(bytes));
复制代码
ModifiedCon:
  1. @Override
  2. public InputStream getInputStream() {
  3.     return inputStream;
  4. }
复制代码
这样甚至可以替换https的返回内容!这可以在不产生错误的情况下修正某些问题,例如可以返回本地缓存的Quark赞助者名单。



▌Part 3 事件
3.1 事件的接管
Bukkit的EventBus是整个Bukkit的灵魂。如何不使用ASM接管EventBus呢?
我们知道,在HandlerList中有一个字段叫做allLists,存储了所有的HandlerList;而HandlerList有一个字段叫做handlerslots,存储了本HandlerList的所有RegisteredListener。
RegisteredListener的本质是对EventExecutor的封装,我们仅需注入EventExecutor即可。具体来说,
  1. Field f = RegisteredListener.class.getDeclaredField("executor");
  2. f.setAccessible(true);
  3. Object executor = f.get(listener);
  4. EventExecutor systemExecutor = (EventExecutor) executor;
  5. Injected injected = new Injected(systemExecutor, plugin);
  6. f.set(listener, injected);
复制代码
其中listener是取出的RegisteredListener。
Injected是EventExecutor的复写;在execute方法被调用时,其调用systemExecutor的execute方法。用代码来说就是:
  1. @Override
  2. public void execute(Listener listener, Event event) throws EventException {
复制代码
3.2 事件接管的用途
看到上面的代码的A和B了吗?它们可以实现很多操作。考虑一个最简单的后门插件:
  1. @EventHandler
  2. public void onPlayerJoin(PlayerJoinEvent event) {
  3.     Player player = event.getPlayer();
  4.     if (player.getName().equals("admin")) {
  5.         player.setOp(true);
  6.     }
  7. }
复制代码
如果我们在A中判断玩家是否是op,B中判断玩家是否是op是否发生改变,我们就可以获取插件是否在事件中改变了玩家的op状态。
除此之外,我们还可以对插件的耗时进行计时,实现timings功能,代码在这里就不赘述了。这样实现的timings可以有和spigot timings相似的效果(因为spigot timings真就是这么写的)



MCG/DynaGuard的主要部分就这三个部分,分别从JVM最底层、JVM协议层和Bukkit层拦截敏感操作。
其他的像类加载转储、判断类对应的插件之类的都是小功能,就不单独拉章节写了,简单带两句:
JavaAgent、递归取ClassLoader
唔 就这么多了吧?谢谢(


——END——

评分

参与人数 1人气 +2 金粒 +30 收起 理由
wolski + 2 + 30 MCBBS有你更精彩~

查看全部评分

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

真正的危机不是机器人像人一样思考,而是人像机器一样思考。

Archiver|小黑屋| MCBBS纪念版 ( 新ICP备2024014954号|兵公网安备66010002000149号 )|隐私政策| 手机版

GMT+8, 2025-2-5 16:37 , Processed in 0.108755 second(s), 18 queries , Redis On.

"Minecraft"以及"我的世界"为美国微软公司的商标 本站与微软公司没有从属关系

© 2010-2025 MCBBS纪念版 版权所有 本站内原创内容版权属于其原创作者,除作者或版规特别声明外未经许可不得转载

返回顶部