Sponsored Link •
|
Advertisement
|
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 Once In Java source code, this looks quite innocuous. The When the Java compiler encounters the Although this is the same process that the virtual machine used to resolve
Because the trusted version of 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, When 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
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 This constraint is checked later, when the virtual machine attempts to resolve the
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 The CD-ROM contains the source code examples from this chapter in the
For more information about the material presented in this chapter, visit the resources page:
Cracker
's symbolic reference to Delegated
, the
virtual machine asks the defining class loader of 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.
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();
}
}
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.
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
.
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
.
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.
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
.
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
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.
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:
Spoofed
for which the system class loader
(Delegated
defining class loader) is marked as an initiating loader must be the same
type named Spoofed
for which GreeterClassLoader
(Cracker
's defining class loader) is marked as an initiating class loader.
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)
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
linking
directory.
The Resources Page
http://www.artima.com/insidejvm/resources/
Sponsored Links
|