【警告】 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—— |