Java Agent: channeling

1. Channeling

In Naruto, the art of channeling belongs to time and space ninja.

So what does "channeling" mean in the Java world? Is to export the class es in the running JVM.

The main purpose of this paper is to export the class file from the JVM with the help of Java Agent.

2. Preparation

Development environment:

  • JDK version: Java 8
  • Development tools: Notepad or vi

Create file directory structure: prepare a prepare SH file

#!/bin/bash

mkdir -p application/{src,out}/sample/
touch application/src/sample/{HelloWorld.java,Program.java}

mkdir -p java-agent/{src,out}/
touch java-agent/src/{ClassDumpAgent.java,ClassDumpTransformer.java,ClassDumpUtils.java,manifest.txt}

mkdir -p tools-attach/{src,out}/
touch tools-attach/src/Attach.java

Directory structure: (before compilation)

java-agent-summoning-jutsu
├─── application
│    └─── src
│         └─── sample
│              ├─── HelloWorld.java
│              └─── Program.java
├─── java-agent
│    └─── src
│         ├─── ClassDumpAgent.java
│         ├─── ClassDumpTransformer.java
│         ├─── ClassDumpUtils.java
│         └─── manifest.txt
└─── tools-attach
     └─── src
          └─── Attach.java

Directory structure: (after compilation)

java-agent-summoning-jutsu
├─── application
│    ├─── out
│    │    └─── sample
│    │         ├─── HelloWorld.class
│    │         └─── Program.class
│    └─── src
│         └─── sample
│              ├─── HelloWorld.java
│              └─── Program.java
├─── java-agent
│    ├─── out
│    │    ├─── ClassDumpAgent.class
│    │    ├─── classdumper.jar
│    │    ├─── ClassDumpTransformer.class
│    │    ├─── ClassDumpUtils.class
│    │    └─── manifest.txt
│    └─── src
│         ├─── ClassDumpAgent.java
│         ├─── ClassDumpTransformer.java
│         ├─── ClassDumpUtils.java
│         └─── manifest.txt
└─── tools-attach
     ├─── out
     │    └─── Attach.class
     └─── src
          └─── Attach.java

3. Application

3.1. HelloWorld.java

package sample;

public class HelloWorld {
    public static int add(int a, int b) {
        return a + b;
    }

    public static int sub(int a, int b) {
        return a - b;
    }
}

3.2. Program.java

package sample;

import java.lang.management.ManagementFactory;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class Program {
    public static void main(String[] args) throws Exception {
        // (1) print process id
        String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();
        System.out.println(nameOfRunningVM);

        // (2) count down
        int count = 600;
        for (int i = 0; i < count; i++) {
            String info = String.format("|%03d| %s remains %03d seconds", i, nameOfRunningVM, (count - i));
            System.out.println(info);

            Random rand = new Random(System.currentTimeMillis());
            int a = rand.nextInt(10);
            int b = rand.nextInt(10);
            boolean flag = rand.nextBoolean();
            String message;
            if (flag) {
                message = String.format("a + b = %d", HelloWorld.add(a, b));
            }
            else {
                message = String.format("a - b = %d", HelloWorld.sub(a, b));
            }
            System.out.println(message);

            TimeUnit.SECONDS.sleep(1);
        }
    }
}

3.3. Compile and run

To compile:

# Compile
$ cd application/
$ javac src/sample/*.java -d out/

Operation results:

$ cd out/
$ java sample.Program
5556@LenovoWin7
|000| 5556@LenovoWin7 remains 600 seconds
a - b = 6
|001| 5556@LenovoWin7 remains 599 seconds
a - b = -4
...

4. Java Agent

There was once an article "retrieving. Class files from a running app", which was first published on the website of Sun company, then transferred to the website of Oracle, and then disappeared from the Oracle website.

Sometimes it is better to dump .class files of generated/modified classes for off-line debugging -
for example, we may want to view such classes using tools like jclasslib.

4.1. class

ClassDumpAgent

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.util.ArrayList;
import java.util.List;

/**
 * This is a java.lang.instrument agent to dump .class files
 * from a running Java application.
 */
public class ClassDumpAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        agentmain(agentArgs, inst);
    }

    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("agentArgs: " + agentArgs);
        ClassDumpUtils.parseArgs(agentArgs);
        inst.addTransformer(new ClassDumpTransformer(), true);
        // by the time we are attached, the classes to be
        // dumped may have been loaded already.
        // So, check for candidates in the loaded classes.
        Class[] classes = inst.getAllLoadedClasses();
        List<Class> candidates = new ArrayList<>();
        for (Class c : classes) {
            String className = c.getName();

            // Step 1: exclude: do not consider the JDK's own classes
            if (className.startsWith("java")) continue;
            if (className.startsWith("javax")) continue;
            if (className.startsWith("jdk")) continue;
            if (className.startsWith("sun")) continue;
            if (className.startsWith("com.sun")) continue;

            // The second step, filtering method: leave only the classes of interest (regular expression matching)
            boolean isModifiable = inst.isModifiableClass(c);
            boolean isCandidate = ClassDumpUtils.isCandidate(className);
            if (isModifiable && isCandidate) {
                candidates.add(c);
            }

            // Unimportant: print debugging information
            String message = String.format("[DEBUG] Loaded Class: %s ---> Modifiable: %s, Candidate: %s", className, isModifiable, isCandidate);
            System.out.println(message);
        }
        try {
            // Step 3: dump the specific class
            // if we have matching candidates, then retransform those classes
            // so that we will get callback to transform.
            if (!candidates.isEmpty()) {
                inst.retransformClasses(candidates.toArray(new Class[0]));

                // Unimportant: print debugging information
                String message = String.format("[DEBUG] candidates size: %d", candidates.size());
                System.out.println(message);
            }
        }
        catch (UnmodifiableClassException ignored) {
        }
    }
}

ClassDumpTransformer

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

public class ClassDumpTransformer implements ClassFileTransformer {

    public byte[] transform(ClassLoader loader,
                            String className,
                            Class redefinedClass,
                            ProtectionDomain protDomain,
                            byte[] classBytes) {
        // check and dump .class file
        if (ClassDumpUtils.isCandidate(className)) {
            ClassDumpUtils.dumpClass(className, classBytes);
        }

        // we don't mess with .class file, just return null
        return null;
    }

}

ClassDumpUtils

import java.io.File;
import java.io.FileOutputStream;
import java.util.regex.Pattern;

public class ClassDumpUtils {
    // directory where we would write .class files
    private static String dumpDir;
    // classes with name matching this pattern will be dumped
    private static Pattern classes;

    // parse agent args of the form arg1=value1,arg2=value2
    public static void parseArgs(String agentArgs) {
        if (agentArgs != null) {
            String[] args = agentArgs.split(",");
            for (String arg : args) {
                String[] tmp = arg.split("=");
                if (tmp.length == 2) {
                    String name = tmp[0];
                    String value = tmp[1];
                    if (name.equals("dumpDir")) {
                        dumpDir = value;
                    }
                    else if (name.equals("classes")) {
                        classes = Pattern.compile(value);
                    }
                }
            }
        }
        if (dumpDir == null) {
            dumpDir = ".";
        }
        if (classes == null) {
            classes = Pattern.compile(".*");
        }
        System.out.println("[DEBUG] dumpDir: " + dumpDir);
        System.out.println("[DEBUG] classes: " + classes);
    }

    public static boolean isCandidate(String className) {
        // ignore array classes
        if (className.charAt(0) == '[') {
            return false;
        }
        // convert the class name to external name
        className = className.replace('/', '.');
        // check for name pattern match
        return classes.matcher(className).matches();
    }

    public static void dumpClass(String className, byte[] classBuf) {
        try {
            // create package directories if needed
            className = className.replace("/", File.separator);
            StringBuilder buf = new StringBuilder();
            buf.append(dumpDir);
            buf.append(File.separatorChar);
            int index = className.lastIndexOf(File.separatorChar);
            if (index != -1) {
                String pkgPath = className.substring(0, index);
                buf.append(pkgPath);
            }
            String dir = buf.toString();
            new File(dir).mkdirs();
            // write .class file
            String fileName = dumpDir + File.separator + className + ".class";
            FileOutputStream fos = new FileOutputStream(fileName);
            fos.write(classBuf);
            fos.close();
            System.out.println("[DEBUG] FileName: " + fileName);
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
    }

}

4.2. manifest.txt

Premain-Class: ClassDumpAgent
Agent-Class: ClassDumpAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

Note: add a blank line at the end.

4.3. Compilation and packaging

The first step is to compile:

$ javac src/ClassDump*.java -d ./out

On the Windows operating system, if you encounter the following error:

error: code GBK Unmapped character of

You can add the - encoding option:

javac -encoding UTF-8 src/ClassDump*.java -d ./out

Step 2: generate Jar file:

$ cp src/manifest.txt out/
$ cd out/
$ jar -cvfm classdumper.jar manifest.txt ClassDump*.class

5. Tools Attach

To connect an Agent Jar with a running Application, you need to use the Attach mechanism:

Agent Jar ---> Tools Attach ---> Application(JVM)

Classes related to the Attach mechanism are defined in tools Jar file:

JDK_HOME/lib/tools.jar

5.1. Attach

import com.sun.tools.attach.VirtualMachine;

/**
 * Simple attach-on-demand client tool
 * that loads the given agent into the given Java process.
 */
public class Attach {
    public static void main(String[] args) throws Exception {
        if (args.length < 2) {
            System.out.println("usage: java Attach <pid> <agent-jar-full-path> [<agent-args>]");
            System.exit(1);
        }
        // JVM is identified by process id (pid).
        VirtualMachine vm = VirtualMachine.attach(args[0]);
        String agentArgs = (args.length > 2) ? args[2] : null;
        // load a specified agent onto the JVM
        vm.loadAgent(args[1], agentArgs);
        vm.detach();
    }
}

5.2. compile

# Compile (Linux)
$ javac -cp "${JAVA_HOME}/lib/tools.jar":. src/Attach.java -d out/

# Compile (MINGW64)
$ javac -cp "${JAVA_HOME}/lib/tools.jar"\;. src/Attach.java -d out/

# Compile (Windows)
$ javac -cp "%JAVA_HOME%/lib/tools.jar";. src/Attach.java -d out/

5.3. function

# Run (Linux)
java -cp "${JAVA_HOME}/lib/tools.jar":. Attach <pid> <full-path-of-classdumper.jar> dumpDir=<dir>,classes=<name-pattern>

# Operation (MINGW64)
java -cp "${JAVA_HOME}/lib/tools.jar"\;. Attach <pid> <full-path-of-classdumper.jar> dumpDir=<dir>,classes=<name-pattern>

# Run (Windows)
java -cp "%JAVA_HOME%/lib/tools.jar";. Attach <pid> <full-path-of-classdumper.jar> dumpDir=<dir>,classes=<name-pattern>

Example:

java -cp "${JAVA_HOME}/lib/tools.jar"\;. Attach <pid> \
D:/tmp/java-agent-summoning-jutsu/java-agent/out/classdumper.jar \
dumpDir=D:/tmp/java-agent-summoning-jutsu/dump,classes=sample\.HelloWorld

6. Summary

The contents of this paper are summarized as follows:

  • First, the main functions. From the perspective of function, it is how to export a class file from a running JVM to the disk.
  • Second, the implementation method. In terms of implementation, it realizes the function with the help of Java Agent and regular expression (distinguishing class name).
  • Third, precautions. In the Java 8 environment, tools are needed to load the Agent Jar into a running JVM jar.

Of course, exporting the class file from the running JVM is only a small part of the functions of Java Agent. You can learn more about Java Agent< Java Agent Basics>.

Keywords: Java

Added by davidppppppppppp on Mon, 07 Mar 2022 13:33:18 +0200