Tricking The Debugger
Some time ago, I had to debug a rather tricky issue. Certain corner cases caused a Java Agent to corrupt parts of the Java class file. Specifically, the part that holds metadata that tells the debugger what to do. This caused part of the debugger to fail. After quite an extensive search through the IntelliJ source and the Java source code, as well as way too much time spent on the Java language specification, I was able to figure out the problem.
But how could somebody (mis)-use this information? Let’s imagine a simple program like this:
public static void main(String[] arg) {
for(int i = 0; i <= 5; ++i) {
String s = "Hello";
s = s + " ";
s = s + "world";
s = s + "!";
System.out.println(s);
}
}
What would we need to do to let an attached debugger think that this is running in reverse? Would that even be possible? First of all, we need to look at the class file format and see what we can work with. There are two parts of the class file we’re interested it. The local variable table and the line Number table.
Local Variable table
Lets look at the local varible table first. To see the class file we can use the tool javap the command to show the full class file with all information would look like this: javap -v {path/to/class/file}. The Local Variable Table is optional, and debug information is not necessarily present in the class file. To add debug information to the class file, you need to run the javac command like this: javac -g {path/to/java/file} So if we look at the Local Variable Table of our main method we will see this.
LocalVariableTable:
Start Length Slot Name Signature
10 28 2 s Ljava/lang/String;
2 42 1 i I
0 45 0 arg [Ljava/lang/String;
Ok, so what does this mean? Let’s go over the attributes one by one. The start and the length values tell a debugger in what scope a local variable is present. Those values refer to the bytecode instructions. So if we look at the generated bytecode:
0: iconst_0
1: istore_1
2: iload_1
3: iconst_5
4: if_icmpgt 44
7: ldc #7 // String Hello
9: astore_2
10: aload_2
11: invokedynamic #9, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
16: astore_2
17: aload_2
18: invokedynamic #13, 0 // InvokeDynamic #1:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
23: astore_2
24: aload_2
25: invokedynamic #14, 0 // InvokeDynamic #2:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
30: astore_2
31: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
34: aload_2
35: invokevirtual #21 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
38: iinc 1, 1
41: goto 2
44: return
We can see that our variable i has a scope from instruction 2 to 44. Which is just what we would expect. The variable i has scope over almost the full function. The next value is the Slot. This refers to the index in the Local Variable array of the current frame that a variable occupies. Doubles and longs use up two indexes, and those slots can be reused. A non-static method will have THIS in Slot 0 Our variable i has the slot position 1. It is the second variable in our function after the parameter so this makes perfect sense as well.
The Name and the Signature are what the name would suggest. Most debuggers will use the name in the Local Variable Table even if the variable had a different name at compile time. The signature is also very important the debugger needs this bit of information to be able to get the variable value from the JVM. For this the debugger uses the JVMTI (JVM Tool Interface) either direcetly or more likey through some other part of the Java Platform Debugger Architecture (JPDA). For anyone interested, the JVMTI provides different functions to get and set local variables depending on the type and the debugger needs to select the correct one.
Line Number Table
Now, onto the second attribute. If we use the same Java code from earlier, the attribute will look like this:
LineNumberTable:
line 6: 0
line 7: 7
line 8: 10
line 9: 17
line 10: 24
line 11: 31
line 6: 38
line 13: 44
The value on the left refers to the line number, and the value on the right refers to the bytecode instruction. Much easier, the debugger uses those values to highlight the correct line number.
Change The Metadata
Now the interesting part is if you would check Chapter 4. The class File Format very careful; you would discover that those two attributes are very loosely defined and are hardly checked. The only things that are checked are if they point to a valid point in the bytecode instruction set, and secondly, this metadata is used only by the debugger. Nothing present in those attributes has any impact on the actual execution of the program. Now with all of this new knowledge, what do we need to do to let the program run in reverse?
Replacing The Code
Even if we would change all of the metadata it would still not be convincing that the code is running in reverse. So we would need to change the code at runtime. For this you would typically use the Instrumentation interface. This provides you with a handy way to replace class files when they are loaded by the JVM. We would change your sample code
public static void main(String[] arg) {
for (int i = 0; i <= 5; i++) {
String s = "Hello";
s += " ";
s += "world";
s += "!";
System.out.println(s);
}
to this:
public static void main(String[] arg) {
for(int i = 5; i >= 0; --i) {
String s = "Hello world!";
s = "Hello world";
s = "Hello ";
s = "Hello";
s = "";
System.out.println("Hello world!");
}
}
If you try to debug the program now the debugger would compline with a message like “Source code does not match the bytecode”. This is because the metadata that the debugger has available does not match the information that the debugger gets from the JVM. But if we change the metadata just right, you won’t see this warning.
Tricking The Debugger
The Java language specification tells us that the elements in the Line Number Table may map to the same line number multiple times. It does not tell us that the instructions need to be in chronological order. But why would it? To change the attribute, the easiest thing is to set up a Java Agent with ASM and write an implementation for the visitLineNumber method. If you’re lazy like me and only want to have a proof of concept, your code will look something like this.
@Override
public void visitLineNumber(int line, Label label) {
switch (line) {
case 12:
super.visitLineNumber(7, label);
break;
case 11:
super.visitLineNumber(8, label);
break;
case 10:
super.visitLineNumber(9, label);
break;
case 9:
super.visitLineNumber(10, label);
break;
case 8:
super.visitLineNumber(11, label);
break;
case 6:
super.visitLineNumber(6, label);
break;
}
}
For my sample code, this remaps all of the elements in reverse order. The Local Variable Table can be a bit more tricky. The scope tells the debugger how long a variable has been present. If you remove a variable from this attribute, it will be invisible to the debugger. Furthermore, one thing to be careful about is if the actual variable type that is present on the frame and the signature in the Local Variable Table don’t match up, the JVMTI will throw an error that will cause your debugger to be unable to show any information.
The full setup can be found in this github repo if the gradle task runDebuggerExample
is run in debug mode the programme should appear to be running in reverse.