检测效果如下:
先写个内存马注入的Agent
注入到HttpServlet
中(关于这个不是文章重点)
然后跑起来我写的工具
- 其中红色框内是注入的
Agent
内存马,可以分析出 - 发现上面还有两个内存马结果,这是我模拟的普通内存马,直接写入到代码中做测试的
自动修复
接下来是内存马的修复,自行写一个Java Agent
即可
暂时只处理ApplicationFilterChain
和HttpServlet
的情况(也是最常见的情况)
public class RepairAgent { public static void agentmain(String agentArgs, Instrumentation ins) { ClassFileTransformer transformer = new RepairTransformer(); ins.addTransformer(transformer, true); Class<?>[] classes = ins.getAllLoadedClasses(); for (Class<?> clas : classes) { if (clas.getName().equals("org.apache.catalina.core.ApplicationFilterChain") || clas.getName().equals("javax.servlet.http.HttpServlet")) { try { ins.retransformClasses(clas); } catch (Exception e) { e.printStackTrace(); } } } } }
处理的逻辑并不复杂
- 由于
ApplicationFilterChain
中包含了LAMBDA
所以我直接简化了代码,变成简单的一句internalDoFilter($1,$2)
做修复(慎重选择,为什么这样做我将在总结里解释) - 修改方法的参数需要用
$1 $2
这样表示,不能写req
和resp
- 这里
HttpServlet
的情况稍复杂,其中有两个service
方法,实际上对任何一个进行修改都可以导致内存马的效果,所以我要做的事情是恢复这两个方法,而不是只针对某一个 - 注意任何非
java.lang
下的类都需要完整类名
public class RepairTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { className = className.replace("/", "."); ClassPool pool = ClassPool.getDefault(); if (className.equals("org.apache.catalina.core.ApplicationFilterChain")) { try { CtClass c = pool.getCtClass(className); CtMethod m = c.getDeclaredMethod("doFilter"); m.setBody("{internalDoFilter($1,$2);}"); byte[] bytes = c.toBytecode(); c.detach(); return bytes; } catch (Exception e) { e.printStackTrace(); } } if (className.equals("javax.servlet.http.HttpServlet")) { try { CtClass c = pool.getCtClass(className); CtClass[] params = new CtClass[]{ pool.getCtClass("javax.servlet.ServletRequest"), pool.getCtClass("javax.servlet.ServletResponse"), }; CtMethod m = c.getDeclaredMethod("service", params); m.setBody("{" + " javax.servlet.http.HttpServletRequest request;\n" + " javax.servlet.http.HttpServletResponse response;\n" + "\n" + " try {\n" + " request = (javax.servlet.http.HttpServletRequest) $1;\n" + " response = (javax.servlet.http.HttpServletResponse) $2;\n" + " } catch (ClassCastException e) {\n" + " throw new javax.servlet.ServletException(lStrings.getString(\"http.non_http\"));\n" + " }\n" + " service(request, response);" + "}"); CtClass[] paramsProtected = new CtClass[]{ pool.getCtClass("javax.servlet.http.HttpServletRequest"), pool.getCtClass("javax.servlet.http.HttpServletResponse"), }; CtMethod mProtected = c.getDeclaredMethod("service", paramsProtected); mProtected.setBody("{" + "String method = $1.getMethod();\n" + "\n" + " if (method.equals(METHOD_GET)) {\n" + " long lastModified = getLastModified($1);\n" + " if (lastModified == -1) {\n" + " doGet($1, $2);\n" + " } else {\n" + " long ifModifiedSince;\n" + " try {\n" + " ifModifiedSince = $1.getDateHeader(HEADER_IFMODSINCE);\n" + " } catch (IllegalArgumentException iae) {\n" + " ifModifiedSince = -1;\n" + " }\n" + " if (ifModifiedSince < (lastModified / 1000 * 1000)) {\n" + " maybeSetLastModified($2, lastModified);\n" + " doGet($1, $2);\n" + " } else {\n" + " $2.setStatus(javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED);\n" + " }\n" + " }\n" + "\n" + " } else if (method.equals(METHOD_HEAD)) {\n" + " long lastModified = getLastModified($1);\n" + " maybeSetLastModified($2, lastModified);\n" + " doHead($1, $2);\n" + "\n" + " } else if (method.equals(METHOD_POST)) {\n" + " doPost($1, $2);\n" + "\n" + " } else if (method.equals(METHOD_PUT)) {\n" + " doPut($1, $2);\n" + "\n" + " } else if (method.equals(METHOD_DELETE)) {\n" + " doDelete($1, $2);\n" + "\n" + " } else if (method.equals(METHOD_OPTIONS)) {\n" + " doOptions($1, $2);\n" + "\n" + " } else if (method.equals(METHOD_TRACE)) {\n" + " doTrace($1, $2);\n" + "\n" + " } else {\n" + " String errMsg = lStrings.getString(\"http.method_not_implemented\");\n" + " Object[] errArgs = new Object[1];\n" + " errArgs[0] = method;\n" + " errMsg = java.text.MessageFormat.format(errMsg, errArgs);\n" + "\n" + " $2.sendError(javax.servlet.http.HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);\n" + " }" + "}"); byte[] bytes = c.toBytecode(); c.detach(); return bytes; } catch (Exception e) { e.printStackTrace(); } } return new byte[0]; } }
当我们写好了Agent
后,需要加入自动修复的逻辑
List<Result> results = Analysis.doAnalysis(files); if (command.repair) { RepairService.start(results, pid); }
如果分析出了结果,且用户选择了修复功能,才会进入修复逻辑(暂只修复这两个最常见的类)
public static void start(List<Result> resultList, int pid) { logger.info("try repair agent memshell"); for (Result result : resultList) { String className = result.getKey().replace("/", "."); if (className.equals("org.apache.catalina.core.ApplicationFilterChain") || className.equals("javax/servlet/http/HttpServlet")) { try { start(pid); return; } catch (Exception ignored) { } } } }
修复的核心代码:把打包好的Agent
拿过来,做一下Atach
和Load
将字节码替换为正常情况即可
public static void start(int pid) { try { String agent = Paths.get("RepairAgent.jar").toAbsolutePath().toString(); VirtualMachine vm = VirtualMachine.attach(String.valueOf(pid)); logger.info("load agent..."); vm.loadAgent(agent); logger.info("repair..."); vm.detach(); logger.info("detach agent..."); } catch (Exception e) { e.printStackTrace(); } }
注意使用VirtualMachine
等API
需要加入tools.jar
,由于上文已经配置了打包插件,所以可以直接打入Jar
包,使用时候java -jar xxx.jar --pid 000
这样会比较方便
<dependency> <groupId>com.sun.tools</groupId> <artifactId>tools</artifactId> <version>jdk-8</version> <scope>system</scope> <systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath> </dependency>
通过以上这些修复手段可以做到的效果:
- 启动某SpringBoot应用
- 通过
Agent
注入内存马,访问后内存马可用 - 通过工具检测到内存马,尝试修改,使字节码被还原
- 再次访问后内存马失效,不需要重启
总结
关于Dump字节码
经过我的一些测试,使用sa-jdi
库不能保证dump
所有的字节码,会出现莫名其妙的异常,猜测是某些字节码不允许被dump
下来。但测试了常见Tomcat
和SpringBoot
等程序,发现基本没有问题
关于非法字节码
只要是包含LAMBDA
的字节码都是非法字节码,无法正常处理,需要用修改源码后的ASM
来做。这种方式终究不是完美的办法,是否存在能够dump
下来合法字节码的方式呢(经过一些尝试没有找到办法)
关于检测
可以看到,字节码分析的过程比较简单,尤其是Runtime.exec
的普通执行命令内存马,很容易绕过,但个人认为这已足够,因为之前的一些条件已经限制了分析的类是不可能包含Runtime.exec
的黑名单类,且大多数用户都是脚本小子,使用免杀型内存马的可能性不大。大多数用户可能直接用了现成的工具,例如冰蝎型内存马的检测方式已完成,暂时来看这样做是足够的,没有必要加入各种免杀检测手段
关于查杀
使用Agent恢复字节码的修复方式理论上没有问题。但其中的ApplicationFilterChain
类的doFilter
方法中包含了LAMBDA
和匿名内部类,这两者都是Javassist
框架不支持的内容,可以用ASM
来做,但可能难度较高
另外对于普通型内存马的修复,通过Agent技术只能覆盖方法体,不可以增加或删除方法。所以理论上可以根据方法的返回值类型,做返回NULL
的处理进行修复
关于拓展
例如代码中我定义的黑名单和关键字,可以根据实战经验自行添加新的类,以实现更完善的效果。在查杀方面我做了最常见的两种,可以根据实际情况自行添加更多的逻辑
最后