The Artima Developer Community
Sponsored Link

Chapter 8 of Inside the Java Virtual Machine
The Linking Model
by Bill Venners

<<  Page 20 of 20

Advertisement

Example: Type Safety and Loading Constraints

In early implementations of the Java virtual machine, it was possible to confuse Java's type system. A Java application could trick the Java virtual machine into using an object of one type as if it were an object of a different type. This capability makes cracker's happy, because they can potentially spoof trusted classes to gain access to non-public data or change the behavior of methods by replacing them with new versions. For example, if a cracker could write a class and successfully fool the Java virtual machine into thinking it was class SecurityManager, that cracker could potentially break out of the sandbox. The example presented in this section is designed to help you understand the type safety problems that can arise with delegating class loaders, and the loading constraints that appeared in the second edition of the Java virtual machine specification to address the problem.

The type safety problem arises because the multiple namespaces inside a Java virtual machine can share types. If one class loader delegates to another class loader, and the delegated-to class loader defines the type, both class loaders are marked as initiating loaders for that type. The type defined by the delegated-to class loader is shared among all the namespaces of the initiating loaders of the type.

At compile time, a type is uniquely identifiable by its fully qualified name. For example, only one class named Spoofed can exist at compile time. At runtime, however, a fully qualified name is not enough to uniquely identify a type that has been loaded into a Java virtual machine. Because a Java application can have multiple class loaders, and each class loader maintains its own namespace, multiple types with the same fully qualified name can be loaded into the same Java virtual machine. Thus, to uniquely identify a type loaded into a Java virtual machine requires the fully qualified name and the defining class loader.

The type safety problems made possible by this class loader architecture arose from the Java virtual machine's initial reliance on the compile time notion of a type being uniquely identifiable by only its fully qualified name. You can always load two types both named Spoofed into the same Java virtual machine. Each Spoofed class would be defined by different class loader. But with a little finesse, you could fool an early implementation of the Java virtual machine into treating an instance of one Spoofed as if it were an instance of the other Spoofed.

To address this problem, the second edition of the Java virtual machine specification introduced the notion of loading constraints. Loading constraints basically enable the Java virtual machine to enforce type safety based not just on fully qualified name, but also on the defining class loader, without forcing eager class loading. When the virtual machine detects a potential for type confusion during constant pool resolution, it adds a constraint to an internal list of constraints. All future resolutions must satisfy this new constraint, as well as all other constraints in the list.

For an example of the type confusion problem and its loading constraints solution, consider this implementation of a greeter, written by a devious cracker:

// On CD-ROM in file linking/ex8/greeters/Cracker.java
import com.artima.greeter.Greeter;

public class Cracker implements Greeter {

    public void greet() {

        Spoofed spoofed = new Spoofed();

        System.out.println("secret val = "
            + spoofed.giveMeFive());

        spoofed = Delegated.getSpoofed();

        System.out.println("secret val = "
            + spoofed.giveMeFive());
    }
}

Class Cracker is a greeter, like Hello or Salutations of the previous examples, because it implements the com.artima.greeter.Greeter interface. Class Cracker is sitting in the linking/ex8 directory of the CD-ROM, along with other, more well- meaning, greeters.

All the classes from the linking/ex7 directory appear unchanged in linking/ex8, except for GreeterClassLoader, which has been slightly modified. (More on this modification later.) You can invoke Cracker with the Greet method just like any other greeter. From the linking/ex8 directory, you can simply type:

java Greet greeters Cracker

The main() method of Greet will, as it did in the previous examples, create a GreeterClassLoader and invoke its loadClass() method, passing in the name Cracker. GreeterClassLoader's loadClass() method will look in the greeters directory, load Cracker.class, instantiate a new Cracker object, and invoke greet() on it. Cracker's greet() method starts by instantiating a new Spoofed. This is where the plot thickens.

It turns out that there are two implementations of a class named Spoofed. The class file for the "trusted" implementation is sitting in the linking/ex8 directory, where it will be discovered by the system class loader:

// On CD-ROM in file linking/ex8/Spoofed.java
// Trusted version - when asked to give five, gives 5

public class Spoofed {

    private int secretValue = 42;

    public int giveMeFive() {
        return 5;
    }

    static {
        System.out.println(
            "linking/ex8/Spoofed initialized.");
    }
}

The trusted Spoofed declares a private variable, named secretValue, that is initialized to 42. This private variable represents anything that needs to be kept secret: a credit card number, a private key, an amount of e-cash, a reference to the current Policy object, and so on. Because the designers of this class didn't want the rest of the world to have access to the secret value, they made the secretValue variable private. Only the methods of class Spoofed can access secretValue. If you inspect the code to the trusted Spoofed class, you'll see that the designers of Spoofed didn't provide any method that reveals information about secretValue. The only method in Spoofed, giveMeFive(), returns the value 5.

But what if a maladjusted cracker was able to trick the virtual machine that an instance of the trusted Spoofed was really an instance of this class, also named Spoofed, which was written by the cracker:

// On CD-ROM in file linking/ex8/greeters/Spoofed.java
// Malicious version - when asked to give five, this
//     version of Spoofed reveals secret_value

public class Spoofed {

    private int secretValue = 100;

    public int giveMeFive() {
        return secretValue;
    }

    static {
        System.out.println(
            "linking/ex8/greeters/Spoofed initialized.");
    }
}

When this Spoofed class's giveMeFive() method is invoked, it returns secretValue, effectively rendering the value of the private variable public knowledge.

So which version of Spoofed gets used by the Cracker greeter? Cracker deviously attempts to use both. First, Cracker's greet() method loads the malicious Spoofed and executes its greet() method, just to get the feel of it:

Spoofed spoofed = new Spoofed();

System.out.println("secret val = "
    + spoofed.giveMeFive());

The Java compiler translates the new Spoofed() expression into a new bytecode instruction that gives the index of a CONSTANT_Class_info constant pool entry, which represents a symbolic reference to Spoofed. When the virtual machine resolves this reference, it will ask the defining loader of Cracker to load spoofed. The defining loader of Cracker is this version of GreeterClassLoader, which the cracker has had the opportunity to modify:

// On CD-ROM in file
// linking/ex8/COM/artima/greeter/GreeterClassLoader.java
package com.artima.greeter;

import java.io.*;
import java.util.Hashtable;

public class GreeterClassLoader extends ClassLoader {

    // basePath gives the path to which this class
    // loader appends "/.class" to get the
    // full path name of the class file to load
    private String basePath;

    public GreeterClassLoader(String basePath) {

        this.basePath = basePath;
    }

    public synchronized Class loadClass(String className,
        boolean resolveIt) throws ClassNotFoundException {

        Class result;
        byte classData[];

        // Check the loaded class cache
        result = findLoadedClass(className);
        if (result != null) {
            // Return a cached class
            return result;
        }

        // If Spoofed, don't delegate
        if (className.compareTo("Spoofed") != 0) {

            // Check with the system class loader
            try {
                result = super.findSystemClass(className);
                // Return a system class
                return result;
            }
            catch (ClassNotFoundException e) {
            }
        }

        // Don't attempt to load a system file except through
        // the primordial class loader
        if (className.startsWith("java.")) {
            throw new ClassNotFoundException();
        }

        // Try to load it from the basePath directory.
        classData = getTypeFromBasePath(className);
        if (classData == null) {
            System.out.println("GCL - Can't load class: "
                + className);
            throw new ClassNotFoundException();
        }

        // Parse it
        result = defineClass(className, classData, 0,
            classData.length);
        if (result == null) {
            System.out.println("GCL - Class format error: "
                + className);
            throw new ClassFormatError();
        }

        if (resolveIt) {
            resolveClass(result);
        }

        // Return class from basePath directory
        return result;
    }

    private byte[] getTypeFromBasePath(String typeName) {

        FileInputStream fis;
        String fileName = basePath + File.separatorChar
            + typeName.replace('.', File.separatorChar)
            + ".class";

        try {
            fis = new FileInputStream(fileName);
        }
        catch (FileNotFoundException e) {
            return null;
        }

        BufferedInputStream bis = new BufferedInputStream(fis);

        ByteArrayOutputStream out = new ByteArrayOutputStream();

        try {
            int c = bis.read();
            while (c != -1) {
                out.write(c);
                c = bis.read();
            }
        }
        catch (IOException e) {
            return null;
        }

        return out.toByteArray();
    }
}

To create this user-defined class loader, the cracker took the GreeterClassLoader from the linking/ex6 directory of the CD-ROM (the one that overrides loadClass()), and added one if statement:

// If Spoofed, don't delegate
if (className.compareTo("Spoofed") != 0) {

    // Check with the system class loader
    try {
        result = super.findSystemClass(className);
        // Return a system class
        return result;
    }
    catch (ClassNotFoundException e) {
    }
}

If the type name passed to loadClass() is "Spoofed", the loadClass() method doesn't first delegate to the system class loader before attempting to load the class in its custom way, by looking in the basePath directory. As a result, when the virtual machine asks this class loader (Cracker's defining class loader) to load Spoofed, its loadClass() doesn't delegate. It just looks in the basePath directory for Spoofed.class, where it finds and loads the definition of the malicious Spoofed. The application prints:

linking/ex8/greeters/Spoofed initialized.

The next statement in Cracker's greet() method invokes giveMeFive() on the new Spoofed instance and prints its return value:

secret val = 100

Having exercised the giveMeFive() method and feeling smug, Cracker's greet() method invokes a static method in a class named Delegated, which returns a reference of type Spoofed:

spoofed = Delegated.getSpoofed();

The Java compiler transforms the Delegated.getSpoofed() expression in the source code to an invokestatic bytecode instruction that gives the index of a CONSTANT_Methodref_info entry in the constant pool. To execute this instruction, the virtual machine must resolve the constant pool entry. As the first step in resolving this symbolic reference to getSpoofed(), the virtual machine resolves the CONSTANT_Class_info reference whose index is given in the class_index of the CONSTANT_Methodref_info entry. The CONSTANT_Class_info entry is a symbolic reference to class Delegated.

To resolve Cracker's symbolic reference to Delegated, the virtual machine asks the defining class loader of Cracker to load Delegated. Once again the virtual machine invokes GreeterClassLoader's loadClass() method, this time passing in the name Delegated. However, because the requested name isn't "Spoofed", the loadClass() method goes ahead and delegates the load request to the system class loader. Because Delegated.class is sitting in the linking/ex8 directory, the system class loader is able to load the class. The system class loader is marked as the defining class loader for Delegated, and both the system class loader and the GreeterClassLoader are marked as initiating class loaders.

Once Delegated has been loaded, the virtual machine completes the resolution of the CONSTANT_Methodref_info and invokes the getSpoofed() method. Here's what Delegated's getSpoofed() method looks like:

// On CD-ROM in file linking/ex8/Delegated.java

public class Delegated {

    public static Spoofed getSpoofed() {

        return new Spoofed();
    }
}

In Java source code, this looks quite innocuous. The getSpoofed() method merely instantiates yet another Spoofed object and returns a reference to it. Inside the Java virtual machine, however, a serious challenge to Java's guarantee of type safety is looming.

When the Java compiler encounters the new Spoofed() expression in class Delegated, it generates a new bytecode that gives the index of a CONSTANT_Class_info that forms a symbolic reference to Spoofed. This is exactly what happened when the Java compiler encountered the new Spoofed() expression in class Cracker. When the Java virtual machine executes this new instruction, just as when it executed the new instruction in Cracker's greet() method, it starts by resolving the symbolic reference to Spoofed. The virtual machine asks Delegated's defining loader, which is the system class loader, to load Spoofed.

Although this is the same process that the virtual machine used to resolve Cracker's symbolic reference to Spoofed, the class loader to which the virtual machine makes its load request is different. Because Cracker's defining loader was GreeterClassLoader, the virtual machine asked GreeterClassLoader to load Spoofed. But because Delegated's defining loader was the system class loader, the virtual machine now asks the system class loader to load Spoofed.

Because the trusted version of Spoofed is sitting in the linking/ex8 directory of the CD-ROM, the system class loader is able to read in the bytes of the Spoofed.class and pass them to defineClass(). What happens next depends on whether or not the application is running in a Java virtual machine that adheres to the loading constraints specified in the second edition of the Java virtual machine specification.

Assume for a moment that the application is running in an early Java virtual machine implementation that doesn't apply the loading constraints. In that case, defineClass() is able to define the type from the bytes read in from linking/ex2/Spoofed.class. The virtual machine creates a new instance of this trusted Spoofed type. Shortly thereafter, Delegated's getSpoofed() method returns a reference to the trusted Spoofed object to its caller, Cracker's greet() method. Cracker stores this reference in local variable spoofed, and proceeds to print out the value returned by invoking giveMeFive() on spoofed.

When Cracker.java was compiled, the Java compiler transformed this second giveMeFive() invocation into yet another invokevirtual instruction that references a CONSTANT_Methodref_info entry in the constant pool, the symbolic reference to giveMeFive() in Spoofed. When the virtual machine goes to resolve this symbolic reference, however, it discovers it has already been resolved. The CONSTANT_Methodref_info entry specified by the second giveMeFive() invocation is the same as that specified by the first one, which was resolved to the malicious Spoofed's implementation of giveMeFive(). The virtual machine invokes the malicious Spoofed method on the trusted Spoofed object, and the application prints:

secret val = 42

Although this kind of type confusion attack was possible in many implementations of the Java virtual machine prior to version 1.2, it usually couldn't be exploited in practice, because it requires the assistance of the class loader. In this example, the cracker added an if statement to GreeterClassLoader's loadClass() method that causes it to treat Spoofed specially. Were the cracker to attempt to instigate this kind of type confusion attack via an untrusted applet, he or she would run into trouble. Untrusted applets are not allowed to create class loaders. Thus, providing the designers of the class loaders in the application that loads applets into browsers did their jobs correctly, the cracker would have no way to exploit this (former) weakness in Java's type safety guarantee.

In Java virtual machine implementations that check the loading constraints that are now part of the Java virtual machine specification, the type confusion is not possible at all. All virtual machines must now keep an internal list of loading constraints that must be met as types are loaded. For example, when such a virtual machine resolves the CONSTANT_Methodref_info entry in Cracker' s constant pool that forms a symbolic reference to the getSpoofed() method of class Delegated, the virtual machine records a loading constraint. Because Delegated was defined by a different class loader than Cracker, and Delegated's getSpoofed() method returns a reference to a Spoofed, the virtual machine records the following constraint:

This constraint is checked later, when the virtual machine attempts to resolve the CONSTANT_Class_info entry in Delegated's constant pool that forms a symbolic reference to class Spoofed. At that time, the virtual machine discovers that the constraint is violated. The type named Spoofed that is being loaded by the system class loader is not the same type named Spoofed that was loaded by GreeterClassLoader. As a result, the Java virtual machine throws a LinkageError:

Exception in thread "main" java.lang.LinkageError: Class Spoofed
violates loader constraints
	at java.lang.ClassLoader.defineClass0(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:422)
	at
java.security.SecureClassLoader.defineClass(SecureClassLoader.java:10)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:248)
	at java.net.URLClassLoader.access$1(URLClassLoader.java:216)
	at java.net.URLClassLoader$1.run (URLClassLoader.java:197)
	at java.security.AccessController.doPrivileged (Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:191)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:290)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:275)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
	at Delegated.getSpoofed(Delegated.java, Compiled Code)
	at Cracker.greet(Cracker.java:13)
	at Greet.main(Greet.java, Compiled Code)

Java's guarantee of type safety is a cornerstone of its security model. Type safety means that programs are allowed to manipulate the memory occupied by an object's instance variables on the heap only in ways that are defined by that object's class. Likewise, type safety means that programs are allowed to manipulate the memory occupied by a class's static variables in the method area only in ways that are defined by that class. If the virtual machine can become confused about types, as demonstrated in this example, malicious code can potentially look at or change non-public variables. In addition, if malicious code could use a method defined in one version of a type to set an int instance variable, then use a method in another version of that type to interpret and return the value of the int as an array, the malicious code would in effect transform an int to an array reference. With this forged pointer, the malicious code could wreak all kinds of havoc. Thus, it is important that Java's type safety guarantee be iron-clad. The loading constraints ensure that, even in the presence of multiple namespaces, Java's type safety will be enforced at runtime.

On the CD-ROM

The CD-ROM contains the source code examples from this chapter in the linking directory.

The Resources Page

For more information about the material presented in this chapter, visit the resources page: http://www.artima.com/insidejvm/resources/

<<  Page 20 of 20


Sponsored Links



Google
  Web Artima.com   
Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use