JVM bytecode execution engine written in Java/Kotlin

JAVA Bytecode Execution Engine

Traditional Java dynamic debugging can only be based on the source code level. If there is no source code or obfuscated Java class file, dynamic debugging cannot be performed.

The operation of a Java program is based on the JVM virtual machine. The JVM virtual machine uses bytecode as the basis for execution. We have constructed a JVM bytecode execution engine using Kotlin, which allows us to debug Java programs at the bytecode level using modern IDEs such as IDEA to observe the program's runtime behavior.

Note that this project is only for studying and researching the running principles of the JVM and analyzing malicious programs. It is strictly prohibited to use it for illegal purposes.

Pre-Knowledge Foundation

Before using this project, please make sure you have the following knowledge foundation.

  1. Understanding the format of Java class files
  2. Understanding the roles and meanings of each bytecode in JVM

Debugging at the bytecode level using IDEA

git clone https://github.com/vlinx-io/vlx-vmengine-jvm.git

Open this project using IDEA (requires JDK 17), and go to TestCases.

There are two test cases in TestCases, one for executing static methods and one for executing instance methods, respectively.executeStaticMethodandexecuteVirtualMethod,

Fill in the corresponding methodclassPath, className~, methodNametranslate the text delimited by triple ~ from zh to en, keep the delimiter in the result, just translate, do not give any additional information: methodSignatureThese informations, Detailed information about class files can be accessed usingClassViewerView.

Just run

For example, take the compiled class file for the following piece of code

public class Hello {

    public void hello() {
        System.out.println("hello");
    }

    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

ExecuteexecuteVirtualMethodRun the hello method of this class

val classPath = "your-classpath"
val className = "Hello"
val methodName = "hello"
val methodSignature = "()V"
val args = listOf<Any?>()

val url = File(classPath).toURI().toURL()
val urls = arrayOf(url)
val loader = VlxClassLoader(urls)

val clazz = loader.loadClass(className)

val method = ClassUtils.getMethod(clazz, methodName, methodSignature, loader)
val instance = clazz.getDeclaredConstructor().newInstance()


val thread = VMThread(VMEngine.instance, loader)
thread.execute(instance, method!!, args, true, 0)

You can get the following output in the console

2023-05-21 17:51:10 [DEBUG] Execute method: public void Hello.hello()
2023-05-21 17:51:10 [DEBUG] Receiver: Hello@3daf7722
2023-05-21 17:51:10 [DEBUG] Args: []
2023-05-21 17:51:11 [DEBUG] LocalVars: [kotlin.Unit]
2023-05-21 17:51:11 [DEBUG] "L0: GETSTATIC"
2023-05-21 17:51:11 [DEBUG] "#7"
2023-05-21 17:51:11 [DEBUG] public static final java.io.PrintStream java.lang.System.out
2023-05-21 17:51:11 [DEBUG] "push" org.gradle.internal.io.LinePerThreadBufferingOutputStream@6aa3a905
2023-05-21 17:51:11 [DEBUG] "L3: LDC"
2023-05-21 17:51:11 [DEBUG] "#13"
2023-05-21 17:51:11 [DEBUG] "hello"
2023-05-21 17:51:11 [DEBUG] "push" "hello"
2023-05-21 17:51:11 [DEBUG] "L5: INVOKEVIRTUAL"
2023-05-21 17:51:11 [DEBUG] "#15"
2023-05-21 17:51:11 [DEBUG] "class java.io.PrintStream, NameAndType(name='println', type='(Ljava/lang/String;)V')"
2023-05-21 17:51:11 [DEBUG] public void java.io.PrintStream.println(java.lang.String)
2023-05-21 17:51:11 [DEBUG] "pop" "hello"
2023-05-21 17:51:11 [DEBUG] "pop" org.gradle.internal.io.LinePerThreadBufferingOutputStream@6aa3a905
2023-05-21 17:51:11 [DEBUG] 	Execute method: public void org.gradle.internal.io.LinePerThreadBufferingOutputStream.println(java.lang.String)
2023-05-21 17:51:11 [DEBUG] 	Receiver: org.gradle.internal.io.LinePerThreadBufferingOutputStream@6aa3a905
2023-05-21 17:51:11 [DEBUG] 	Args: [org.gradle.internal.io.LinePerThreadBufferingOutputStream@6aa3a905, hello]
2023-05-21 17:51:11 [ERROR] Can't parse class class org.gradle.internal.io.LinePerThreadBufferingOutputStream
hello
2023-05-21 17:51:11 [DEBUG] "L8: RETURN"

The console output displays all the bytecode instructions of this method, the changes in the stack during instruction execution, and the result of each bytecode instruction's execution.

Debugging

If you need to debug bytecode instructions, you can do so by usingVMExecutorIn theexecute()Method breakpoint

 fun execute() {
    while (true) {
        try {
            pc = sequence.index()
            val opcode = sequence.readUnsignedByte().toShort()
            if (execute(opcode)) {
                break
            }
        } catch (vme: VlxVmException) {
            Logger.FATAL(vme)
            exitProcess(1)
        } catch (t: Throwable) {
            handleException(t)
        }

    }
}

Debugging submethod bytecode

By default, the virtual engine only interprets the bytecode of the specified method and the sub-methods called in the specified method are still executed in the JVM, avoiding the huge performance overhead of multiple method invocations. If you want all methods to be interpreted by the virtual engine, please modifyio.vlinx.vmengine.Options,TranslatehandleSubMethodModify to true