Write Up : MCG/DynaGuard:JVM层HIPS的原理与实现
-
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
唔 就这么多了吧?谢谢(
——EOF——