Security: Class Loaders in Java

A Java compiler converts source instructions into code for the Java virtual machine. The virtual machine code is stored in a class file with a .class exten­sion. Each class file contains the definition and implementation code for one class or interface. In the following section, you will see how the virtual machine loads these class files.

1. The Class-Loading Process

The virtual machine loads only those class files that are needed for the exe­cution of a program. For example, suppose program execution starts with MyProgram.class. Here are the steps that the virtual machine carries out:

  1. The virtual machine has a mechanism for loading class files—for example, by reading the files from disk or by requesting them from the Web; it uses this mechanism to load the contents of the MyProgram class file.
  2. If the MyProgram class has fields or superclasses of another class type, their class files are loaded as well. (The process of loading all the classes that a given class depends on is called resolving the class.)
  3. The virtual machine then executes the main method in MyProgram (which is static, so no instance of a class needs to be created).
  1. If the main method or a method that main calls requires additional classes, these are loaded next.

The class loading mechanism doesn’t just use a single class loader, however. Every Java program has at least three class loaders:

  • The bootstrap class loader
  • The platform class loader
  • The system class loader (sometimes called the application class loader)

The bootstrap class loader loads the platform classes contained in the modules

java.base

java.datatransfer

java.desktop

java.instrument

java.logging

java.management

java.management.rmi

java.naming

java.prefs

java.rmi

java.security.sasl

java.xml

as well as a number of JDK-internal modules.

There is no CtassLoader object corresponding to the bootstrap class loader. For example,

StringBuitder.ctass.getCtassLoader()

returns null.

Prior to Java 9, the Java platform classes were located in a file rt.jar. Nowadays, the Java platform is modular, and each platform module is contained in a JMOD file (see Chapter 9). The platform class loader loads all classes of the Java platform that are not loaded by the bootstrap class loader.

The system class loader loads application classes from the module path and class path.

2. The Class Loader Hierarchy

Class loaders have a parent/child relationship. Every class loader except for the bootstrap one has a parent class loader. A class loader will give its parent a chance to load any given class and will only load it if the parent has failed. For example, when the system class loader is asked to load a system class (say, java.lang.StringBuilder), it first asks the platform class loader. That class loader first asks the bootstrap class loader. The bootstrap class loader finds and loads the class, so neither of the other class loaders searches any further.

Some programs have a plugin architecture in which certain parts of the code are packaged as optional plugins. If the plugins are packaged as JAR files, you can simply load the plugin classes with an instance of URLCtassLoader.

var urt = new URL(“file:///path/to/plugin.jar”);

var pluginLoader = new URLClassLoader(new URL[] { url });

Class<?> cl = ptuginLoader.toadCtass(“mypackage.MyCtass”);

Since no parent was specified in the URLCtassLoader constructor, the parent of the pluginLoader is the system class loader. Figure 10.1 shows the hierarchy.

Most of the time, you don’t have to worry about the class loader hierarchy. Generally, classes are loaded because they are required by other classes, and that process is transparent to you.

Occasionally, however, you need to intervene and specify a class loader. Consider this example:

  • Your application code contains a helper method that calls Class.forName( ctassNameString) .
  • That method is called from a plugin class.
  • The ctassNameString specifies a class that is contained in the plugin JAR.

The author of the plugin wants the class to be loaded. However, the helper method’s class was loaded by the system class loader, and that is the class loader used by Class.forName. The classes in the plugin JAR are not visible. This phenomenon is called classloader inversion.

To overcome this problem, the helper method needs to use the correct class loader. It can require the class loader as a parameter. Alternatively, it can require that the correct class loader is set as the context class loader of the current thread. This strategy is used by many frameworks (such as JAXP and JNDI).

Each thread has a reference to a class loader, called the context class loader. The main thread’s context class loader is the system class loader. When a new thread is created, its context class loader is set to the creating thread’s context class loader. Thus, if you don’t do anything, all threads will have their context class loaders set to the system class loader.

However, you can set any class loader by calling

Thread t = Thread.currentThread();

t.setContextCtassLoader(toader);

The helper method can then retrieve the context class loader:

Thread t = Thread.currentThread();

CtassLoader loader = t.getContextCtassLoader();

Ctass<?> cl = loader.loadClass(className);

3. Using Class Loaders as Namespaces

Every Java programmer knows that package names are used to eliminate name conflicts. There are two classes called Date in the standard library, but of course their real names are java.util.Date and java.sql.Date. The simple name is only a programmer convenience and requires the inclusion of appropriate import statements. In a running program, all class names contain their package names.

It might surprise you, however, that you can have two classes in the same virtual machine that have the same class and package name. A class is deter­mined by its full name and the class loader. This technique is useful for loading code from multiple sources. For example, an application server uses separate class loaders for each application. This allows the virtual machine to separate classes from different applications, no matter what they are named. Figure 10.2 shows an example. Suppose an application server loads two dif­ferent applications, and each has a class called Util. Since each class is loaded by a separate class loader, these classes are entirely distinct and do not conflict with each other.

4. Writing Your Own Class Loader

You can write your own class loader for specialized purposes. That lets you carry out custom checks before you pass the bytecodes to the virtual machine. For example, your class loader may refuse to load a class that has not been marked as “paid for.”

To write your own class loader, simply extend the ClassLoader class and override the method

findClass(String className)

The loadClass method of the ClassLoader superclass takes care of the delegation to the parent and calls findClass only if the class hasn’t already been loaded and if the parent class loader was unable to load the class.

Your implementation of this method must do the following:

  1. Load the bytecodes for the class from the local file system or some other source.
  2. Call the defineCtass method of the CtassLoader superclass to present the bytecodes to the virtual machine.

In the program of Listing 10.1, we implement a class loader that loads encrypted class files. The program asks the user for the name of the first class to load (that is, the class containing main) and the decryption key. It then uses a special class loader to load the specified class and calls the main method. The class loader decrypts the specified class and all nonsystem classes that are referenced by it. Finally, the program calls the main method of the loaded class (see Figure 10.3).

For simplicity, we ignore the 2,000 years of progress in the field of crypto­graphy and use the venerable Caesar cipher for encrypting the class files.

Our version of the Caesar cipher has as a key a number between 1 and 255. To decrypt, simply add that key to every byte and reduce modulo 256. The Caesar.java program of Listing 10.2 carries out the encryption.

To not confuse the regular class loader, we use a different extension, .caesar, for the encrypted class files.

To decrypt, the class loader simply subtracts the key from every byte. In the companion code for this book, you will find four class files, encrypted with a key value of 3—the traditional choice. To run the encrypted program, you’ll need the custom class loader defined in our CtassLoaderTest program.

Encrypting class files has a number of practical uses (provided, of course, that you use something stronger than the Caesar cipher). Without the decryption key, the class files are useless. They can neither be executed by a standard virtual machine nor readily disassembled.

This means that you can use a custom class loader to authenticate the user of the class or to ensure that a program has been paid for before it will be allowed to run. Of course, encryption is only one application of a custom class loader. You can use other types of class loaders to solve other problems—for example, storing class files in a database.

5. Bytecode Verification

When a class loader presents the bytecodes of a newly loaded Java platform class to the virtual machine, these bytecodes are first inspected by a verifier. The verifier checks that the instructions cannot perform actions that are obviously damaging. All classes except for system classes are verified.

Here are some of the checks that the verifier carries out:

  • Variables are initialized before they are used.
  • Method calls match the types of object references.
  • Rules for accessing private data and methods are not violated.
  • Local variable accesses fall within the runtime stack.
  • The runtime stack does not overflow.

If any of these checks fails, the class is considered corrupted and will not be loaded.

This strict verification is an important security consideration. Accidental errors, such as uninitialized variables, can easily wreak havoc if they are not caught. More importantly, in the wide open world of the Internet, you must be pro­tected against malicious programmers who create evil effects on purpose. For example, by modifying values on the runtime stack or by writing to the private data fields of system objects, a program can break through the security system of a browser.

You might wonder, however, why a special verifier is needed to check all these features. After all, the compiler would never allow you to generate a class file in which an uninitialized variable is used or in which a private data field is accessed from another class. Indeed, a class file generated by a com­piler for the Java programming language always passes verification. However, the bytecode format used in the class files is well documented, and it is an easy matter for someone with experience in assembly programming and a hex editor to manually produce a class file containing valid but unsafe instruc­tions for the Java virtual machine. The verifier is always guarding against maliciously altered class files—not just checking the class files produced by a compiler.

Here’s an example of how to construct such an altered class file. We start with the program VerifierTest.java of Listing 10.3. This is a simple program that calls a method and displays the method’s result. The program can be run both as a console program and as an applet. The fun method itself just computes 1 + 2.

static int fun()

{

int m;

int n;

m = 1;

n = 2;

int r = m + n;

return r;

}

As an experiment, try to compile the following modification of this program:

static int fun()

{

int m = 1;

int n;

m = 1;

m = 2;

int r = m + n;

return r;

}

Here, n is not initialized, so it could have any random value. Of course, the compiler detects that problem and refuses to compile the program. To create a bad class file, we have to work a little harder. First, run the javap program to find out how the compiler translates the fun method. The command

javap -c verifier.VerifierTest

shows the bytecodes in the class file in mnemonic form.

Method int fun()

0 iconst_1

1 istore_0

2 iconst_2

3 istore_1

4 itoad_0

5 itoad_1

6 iadd

7 istore_2

8 itoad_2

9 ireturn

Use a hex editor to change instruction 3 from istore_1 to istore_0. That is, local variable 0 (which is m) is initialized twice, and local variable 1 (which is n) is not initialized at all. We need to know the hexadecimal values for these in­structions; these values are readily available from the Java Virtual Machine specification (https://docs.oracle.com/iavase/specs/ivms/se11/htmt/index.htmt).

0 iconst_1 04

1 istore_0 3B

2 iconst_2 05

3 istore_1 3C

4 iload_0 1A

5 iload_1 1B

6 iadd 60

7 istore_2 3D

8 iload_2 1C

9 ireturn AC

You can use any hex editor to carry out the modification. In Figure 10.4, you see the class file VerifierTest.class loaded into the Gnome hex editor, with the bytecodes of the fun method highlighted.

Change 3C to 3B and save the class file. Then try running the VerifierTest program. You get an error message:

Exception in thread “main” java.lang.VerifyError: (class: VerifierTest, method:fun signature:

()I) Accessing value from uninitialized register 1

That is good—the virtual machine detected our modification.

Now run the program with the -noverify (or -Xverify:none) option:

java -noverify verifier.VerifierTest

The fun method returns a seemingly random value. This is actually 2 plus the value that happened to be stored in the variable n, which was never initialized. Here is a typical printout:

1 + 2 == 15102330

Source: Horstmann Cay S. (2019), Core Java. Volume II – Advanced Features, Pearson; 11th edition.

Leave a Reply

Your email address will not be published. Required fields are marked *