Moteur d'exécution de bytecode JVM écrit en Java/Kotlin
Moteur d'exécution de bytecode JAVA
Le débogage dynamique traditionnel de Java ne peut être basé que sur le niveau du code source, et ne permet pas de déboguer dynamiquement sans code source ou des fichiers Class Java obfusqués.
L'exécution des programmes Java est basée sur la machine virtuelle JVM, qui exécute du bytecode. Nous avons construit un moteur d'exécution de bytecode JVM en utilisant Kotlin, ce qui nous permet de déboguer les programmes Java au niveau du bytecode en utilisant des IDE modernes tels qu'IntelliJ IDEA pour observer le comportement du programme pendant l'exécution.
Attention, ce projet est uniquement destiné à l'étude et à la recherche sur le principe de fonctionnement de la JVM et à l'analyse de programmes malveillants. Il est strictement interdit de l'utiliser à des fins illégales.
Prérequis de connaissances
Avant d'utiliser ce projet, veuillez vous assurer que vous possédez les connaissances suivantes.
- Comprendre le format des fichiers Class Java
- Comprendre le rôle et la signification de chaque bytecode dans la JVM
Débogage au niveau du bytecode avec IDEA
git clone https://github.com/vlinx-io/vlx-vmengine-jvm.git
Ouvrez ce projet avec IDEA (nécessite JDK 17) et naviguez vers TestCases
Il y a deux cas de test dans TestCases, un pour exécuter des méthodes statiques et un pour exécuter des méthodes d'instance, respectivement nommés executeStaticMethod et executeVirtualMethod.
Sur la méthode correspondante, renseignez les informations telles que classPath, className, methodName et methodSignature. Les informations détaillées du fichier Class peuvent être consultées à l'aide de ClassViewer.
Exécution directe
En utilisant le fichier Class compilé du code suivant comme exemple
public class Hello {
public void hello() {
System.out.println("hello");
}
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
Exécutez executeVirtualMethod, lancez la méthode hello de cette classe.
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)
Vous pouvez obtenir la sortie suivante sur la 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"
La sortie console affiche toutes les instructions bytecode de cette méthode, les changements dans la pile pendant l'exécution des instructions et les résultats de chaque instruction bytecode.
Débogage
Si vous avez besoin de déboguer les instructions bytecode, vous pouvez placer des points d'arrêt avant et après la méthode execute() dans VMExecutor.
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)
}
}
}
Débogage du bytecode des sous-méthodes
Par défaut, le moteur virtuel n'interprète et n'exécute que le bytecode de la méthode spécifiée. Les sous-méthodes appelées au sein de la méthode spécifiée s'exécutent toujours dans la JVM pour éviter une surcharge de performance significative due aux appels multiples. Si vous souhaitez que toutes les méthodes soient interprétées et exécutées par le moteur virtuel, veuillez modifier io.vlinx.vmengine.Options et changer handleSubMethod en true.