Write Up : MCG/DynaGuard:JVM层HIPS的原理与实现
本帖最后由 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是非常简单的。我们只需要使用:
System.setSecurityManager(sm);即可注册。这应该不会有什么问题...吧?
然后问题就来了。因为一个历史遗留问题,Forge会注册SecurityManager,并且cpw拒绝对此行为进行任何修改。
这将会带来一个问题,在Mohist与CatServer等混合核心上,当你尝试使用setSecurityManager时,会失败。(传送门)
让我们想一下如何解决这个问题。首先,我们知道,SecurityManager是System的一个属性。
private static volatile SecurityManager security = null;
那么我们可不可以
Field f = System.class.getDeclaredField("security");
f.setAccessible(true);
f.set(null, sm);答案是不行。因为f是null。为什么呢?其实是因为,getDeclaredField方法使用的是privateGetDeclaredFields,privateGetDeclaredFields中用了Reflection.filterFields;这会干扰我们的取得。
如果我们绕过它呢?getDeclaredFields0,启动?
Method getDeclaredFields0M = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class);
getDeclaredFields0M.setAccessible(true);
Field[] fields = (Field[]) getDeclaredFields0M.invoke(System.class, false);
Field securityField = null;
for (Field field : fields) {
if (field.getName().equals("security")) {
securityField = field;
break;
}
}
securityField.setAccessible(true);
securityField.set(null, mysm);getDeclaredFields0确实不在filterMethods的目录中(疑似是bug);而cpw并没有阻止这个反射操作。因此,我们可以用这个方法绕过Forge对SecurityManager的占用。
思考题:该如何避免我们的SecurityManager被使用相同方法替换?
答案:
方法有很多种。例如,在getDeclaredMethod中,调用了privateGetDeclaredMethods;而privateGetDeclaredMethods调用了Reflection.filterMethods;
Reflection.filterMethods的实现如下:
public static Method[] filterMethods(Class<?> containingClass, Method[] methods) {
if (methodFilterMap == null) {
return methods;
}
return (Method[])filter(methods, methodFilterMap.get(containingClass));
}仅需对methodFilterMap做出修改,禁止getDeclaredMethod被get即可。
1.2 SecurityManager的使用
其实我觉得这章不用写
在注册了SecurityManager后,我们就拥有了几乎全部的生杀夺予大权。举例而言,我们可以对命令执行进行拦截。
@Override
public void checkExec(String cmd) {
throw new SmException("Access denied (exec)");
}当然,您可以判断cmd是否合理;过于简单,这里不再赘述。
类似的,我们可以限制文件读写、网络访问、包可见性等内容。
▌Part 2 URI注入
2.1 URI的注册
我们知道,一个典型的Java的联网代码的写法是:
HttpURLConnection connection = (HttpURLConnection) new URL("http://example.com/?q=is-mcg-present").openConnection();这段代码的执行流程的关键步骤是:
public URLConnection openConnection() throws java.io.IOException {
return handler.openConnection(this);
}handler的定义是在URL的构造器中执行的。具体来说,URL.getURLStreamHandler会根据protocol返回对应的handler。
URL是经典的工厂模式,这是极好的。仅需自己实现InjectURI implements URLStreamHandlerFactory即可。
Field field = URL.class.getDeclaredField("factory");
field.setAccessible(true);
URLStreamHandlerFactory factory = (URLStreamHandlerFactory) field.get(null);
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:
@Override
protected URLConnection openConnection(URL u, Proxy p) throws IOException {
byte[] bytes = // Whatever
if (bytes != null) {
return new ModifiedCon(u, p, new ByteArrayInputStream(bytes));ModifiedCon:
@Override
public InputStream getInputStream() {
return inputStream;
}这样甚至可以替换https的返回内容!这可以在不产生错误的情况下修正某些问题,例如可以返回本地缓存的Quark赞助者名单。
▌Part 3 事件
3.1 事件的接管
Bukkit的EventBus是整个Bukkit的灵魂。如何不使用ASM接管EventBus呢?
我们知道,在HandlerList中有一个字段叫做allLists,存储了所有的HandlerList;而HandlerList有一个字段叫做handlerslots,存储了本HandlerList的所有RegisteredListener。
RegisteredListener的本质是对EventExecutor的封装,我们仅需注入EventExecutor即可。具体来说,
Field f = RegisteredListener.class.getDeclaredField("executor");
f.setAccessible(true);
Object executor = f.get(listener);
EventExecutor systemExecutor = (EventExecutor) executor;
Injected injected = new Injected(systemExecutor, plugin);
f.set(listener, injected);其中listener是取出的RegisteredListener。
Injected是EventExecutor的复写;在execute方法被调用时,其调用systemExecutor的execute方法。用代码来说就是:
@Override
public void execute(Listener listener, Event event) throws EventException {3.2 事件接管的用途
看到上面的代码的A和B了吗?它们可以实现很多操作。考虑一个最简单的后门插件:
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
if (player.getName().equals("admin")) {
player.setOp(true);
}
}如果我们在A中判断玩家是否是op,B中判断玩家是否是op是否发生改变,我们就可以获取插件是否在事件中改变了玩家的op状态。
除此之外,我们还可以对插件的耗时进行计时,实现timings功能,代码在这里就不赘述了。这样实现的timings可以有和spigot timings相似的效果(因为spigot timings真就是这么写的)
MCG/DynaGuard的主要部分就这三个部分,分别从JVM最底层、JVM协议层和Bukkit层拦截敏感操作。
其他的像类加载转储、判断类对应的插件之类的都是小功能,就不单独拉章节写了,简单带两句:
JavaAgent、递归取ClassLoader
唔 就这么多了吧?谢谢(
——END——
页:
[1]