长亭百川云 - 文章详情

内存马检测工具shell-analyzer(2)远程查杀实现

Y4Sec Team

153

2024-07-13

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;`    `// 。。。``}`

使用  等命令获取所有组件信息的时候,需要先进行验证,命令格式为: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) {`    `//...`    `}`  `}`

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

`@Override``public 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 方法继续传递

`@Override``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);``}`

由于在 Tomcat 中 Filters 是一条链,如果这一条链在中间断开,将会导致未知的问题,以至服务不可用。所以需要使用 filterChain.doFilter 方法传递

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

`@Override``public 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

`@Override``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;`    `}``}`

对于 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 方法

`@Override``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;``}`

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 的执行方法

`@Override``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);``}`

被修复后的 Valve 内存马应该长这样

`@Override``public 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 和分析

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2