http://www.developer.com/

Back to article

Java Performance & Security: Dynamically Loaded Classes


January 5, 2007

This series, The Object-Oriented Thought Process, is intended for someone just learning an object-oriented language and who wants to understand the basic concepts before jumping into the code, or someone who wants to understand the infrastructure behind an object-oriented language he or she is already using. These concepts are part of the foundation that any programmer will need to make the paradigm shift from procedural programming to object-oriented programming.

Click here to start at the beginning of the series.

In keeping with the code examples used in the previous articles, Java will be the language used to implement the concepts in code. One of the reasons that I like to use Java is because you can download the Java compiler for personal use at the Sun Microsystems Web site http://java.sun.com/. You can download the standard edition, J2SE 5.0, at http://java.sun.com/j2se/1.5.0/download.jsp to compile and execute these applications. I often reference the Java J2SE 5.0 API documentation and I recommend that you explore the Java API further. Code listings are provided for all examples in this article as well as figures and output (when appropriate). See the first article in this series for detailed descriptions for compiling and running all the code examples.

This column covers the topic of Java Class verification security. Here, you explore the concept of dynamically loaded classes, a very basic aspect of security that a Java developer needs to consider for performance and security reasons.

Dynamically Loaded Classes

As is often the case, a specific system's strength can also be a glaring weakness, or at least a vulnerability. This statement is true in one very important aspect of the Java language as well as the .NET model: their ability to load classes dynamically.

Before the widespread acceptance of object-oriented language development platforms, it was common for applications to be compiled and then statically linked. This model is still in place today for many development paradigms. When coding in the C programming language, for example, you would create (edit) the code, compile it, and then link it. The linking process would create a static, executable application that could then be run on the native machine.

Diagram 1: Static Edit/Compile/Link/Execute Model.

The term "native machine" is an important point here. When you create a Windows EXE file, that file is native to the Windows environment and cannot be transported to a UNIX or Mac platform, at least not without a simulator.

This is an advantage in the sense that the entire application can be packaged in a single executable file. The problem, besides that transportability issue, is that the executable has everything included in the file, including the kitchen sink. In short, you must include everything you potentially need in that executable file. If you need to rebuild or redeploy any part of the executable, you must re-link the entire application, create a new executable, and deploy it at all locations.

The bytecode model deals with things differently. Rather than statically link everything and create a single application, each module creates its own class (the bytecodes) and these classes are dynamically loaded when needed. This is of particular importance when you consider that so many applications are now being run over networks. Just consider the size of some Windows EXE files and you can see that transporting them over a network is not always practical.

Diagram 2: Dynamic Compile/Interpret Model (bytecodes).

However, if you simply load a main application with a small footprint, and then only request the code that the application actually needs (and when it needs it), you have a much more efficient model. Take a look at an example of this dynamic loading.

Dynamic loading

You can use the code example from the previous article on Controlling Access to a Class. In this case, you will use only two of the classes, AccountList and CheckingAccount.

Listing 1: Simple Application

class AccountList {

   public static void main(String args[]) {

      CheckingAccount myList = new CheckingAccount();

   }
}

class CheckingAccount {

   protected String accountNumber;
   protected double accountBalance = 1000000.00;

}

When these classes are compiled, you can then invoke the main class and execute the application. When you inspect the directory (in this case, right off the C: drive) where the application resides (see Figure 1), you see that there are indeed two class files, AccountList.class and CheckingAccount.class.

Figure 1: Individual Class Files.

An important thing to understand here is that the main application class AccountList actually creates a CheckingAccount object.

CheckingAccount myList = new CheckingAccount();

This line is the point when the Virtual Machine actually goes looking for the CheckingAccount class. What happens if the CheckingAccount class is not there? Delete it from the directory. If you then run the application, you get the following error, as seen in Figure 2.

Figure 2: Class CheckingAccount missing.

As you can see, when the main class, AccountList, is loaded and executed, it complains when it can't find the CheckingAccount class. This is because CheckingAccount is literally not there. In fact, the error occurs because AccountList, after it starts, cannot find the CheckingAccount class when it attempts to load it. How can you prove this? Take out the line of code where you create the CheckingAccount object: Listing 2.

Listing 2: No Object Created

class AccountList {

   public static void main(String args[]) {

   }
}

In this example, when the AccountList class is loaded, it does not require the services of the CheckingAccount class. Thus, there is no error generated, as seen in Figure 3.

Figure 3: Class CheckingAccount not needed.

This is an important concept; the classes are not loaded until they are actually needed. Dynamic loading of classes can greatly increase the performance of applications. This includes reducing the flow of bytes over networks as well as keeping memory footprints to a minimum.

Application Integrity

Although dynamically loading classes provides significant performance advantages, there are also some concerns that have to be addressed.

Unintended design inconsistencies

For example, add to the application that you created in the previous section (refer to Listing 2). The output from this application is what you saw in Figure 3. In this updated example (see Listing 3), a getter method is added, getAccountBalance(), so you can properly inspect the account balance.

Listing 3: Sample Application

class AccountList {

   public static void main(String args[]) {

      System.out.println("In AccountList");
      CheckingAccount myList = new CheckingAccount();

      myList.getAccountBalance();

   }
}

class CheckingAccount {

   protected String accountNumber;
   protected double accountBalance = 1000000.00;

   public void getAccountBalance() {
      System.out.println("Account Balance = " + accountBalance);
   }

}

When this code is executed, the output in Figure 4 is produced.

Figure 4: Class CheckingAccount with getter.

Now, assume that there is a change to the CheckingAccount class and it is edited and compiled without updating the AccountList class, either intentionally or accidentally (I will discuss the intentionally possibility a bit more later).

In this case, you decide to change the name of the getter method from getAccountBalance() to getBalance(), as seen in Listing 4.

Listing 4: Altered CheckingAccount class

class AccountList {

   public static void main(String args[]) {

      System.out.println("In AccountList");
      CheckingAccount myList = new CheckingAccount();

      myList.getAccountBalance();

   }
}

class CheckingAccount {

   protected String accountNumber;
   protected double accountBalance = 1000000.00;

   public void getBalance() {
      System.out.println("Account Balance = " + accountBalance);
   }

}

Note the line in bold within the CheckingAccount class:

public void getBalance() {

The name of the getter now is obviously not the same as the original name (getAccountBalance) that still is expected in the AccountList class, that will still class the object's method, as seen below.

myList.getAccountBalance();

The issue now is that the classes are now out of sync—and it was relatively easy for this to happen. This problem can, and will, cause the application to abort—which is always interesting to test, but you never, ever, want this to happen when the application is live. Figure 5 shows the error that is generated when this application is run with the out-of-sync classes.

Figure 5: AccountList can't find method.

When the AccountList class invokes the getAccountBalance() method of the CheckingAccount class, it just isn't there—so the application fails. This is a serious configuration management issue and can cause nightmares when updating clients who already have a working system.

Malicious intent

Although most development inconsistencies are unintentional, unfortunately, there are situations when someone may try to intentionally sabotage an application. Of course, there are many examples that can demonstrate potential risks with code; you will present a situation that lies at the heart of a major security risk with dynamically loaded classes.

Continue with the previous example. Revisit the code in Listing 3, which produces the output in Figure 4. There are only two class files in this application.

AccountList.class
CheckingAccount.class

The CheckingAccount class actually contains the value of the account balance.

protected double accountBalance = 1000000.00;
Note: Remember the code in this example is used for demonstration purposes and that you would not hardcode a value in practice.

What would happen if someone who knew the design of the application decided to subversively increase the value of accountBalance? In this case, this person could in fact create another copy of the CheckingAccount.class and substitute the rogue class for the correct one.

To accomplish this in a very basic strategy, the rogue class simply can be substituted (copied) into the application directory. However, let me do this in a couple of steps to help illustrate the process.

As stated earlier, there are two classes in the application directory:

AccountList.class
CheckingAccount.class

If you delete the CheckingAccount.class from the directory, you obviously have only a single class left:

AccountList.class

You have seen what happens when this class is removed. When the AccountList.class is loaded and the Virtual Machine attempts to load the CheckingAccount.class, an application error is generated, as you saw in Figure 2.

Now, create the rogue CheckingAccount.class, as seen in Listing 5.

Listing 5: Rogue CheckingAccount class

class CheckingAccount {

   protected String accountNumber;
   protected double accountBalance = 2000000.00;

   public void getAccountBalance() {
      System.out.println("Account Balance = " + accountBalance);
   }

}

You may have noticed that the value for the accountBalance attribute has miraculously grown. For this example, I actually created a separate directory and created an entirely new application to develop and test the rogue CheckingAccount class; I will discuss other code substitution possibilities shortly.

To help illustrate the point, Figure 6 shows what the legitimate application directory looks like without the CheckingAccount class, which will generate the error in Figure 2. Note that there is no CheckingAccount class present, only the Java file source code.



Click here for a larger image.

Figure 6: No CheckingAccount class.

Now, copy the rogue CheckingAccount class into the actual application directory. After you do this, the directory will contain the CheckingAccount class, as seen in Figure 7.

Figure 7: Rogue CheckingAccount class inserted.

Just by looking at the directory, there is nothing suspicious. It looks like the original and intended copy of the CheckingAccount class. Yet, you know that this is actually a rogue version of CheckingAccount. If you run the application, you get the output seen in Figure 8.

Figure 8: Rogue CheckingAccount class ouput.

As you can see, the account balance now is valued at 2,000,000, not the intended 1,000,000.

Although this is a very simplistic and contrived example, it illustrates the vulnerability that exists when classes are loaded dynamically. If this were a statically linked application, the only way to hack into the application would be to edit the executable code and alter it directly. On top of that problem, each of the distributed executable files would have to be altered (and distributed) as well. In other words, if a statically linked application is distributed (say, a Windows EXE file) to 100 customers, each of the 100 EXE files would have to be tracked down and altered.

However, if a dynamically loaded class is used (in this case, assume that the class is loaded over an Internet connection, perhaps a web page), theoretically the class need be changed only in one place. This is what is meant by other distribution possibilities. It is not simply a case of creating a rogue class on the same machine, or even with a single project. The rogue class can be created and substituted over an Internet connection or any other network connection. It may even be as simple as substituting in a single location, say a server, because in this case all clients will access the same server and thus the same class.

Diagram 3: Inserting a rogue class file.

There are built-in measures that are in place to allow the virtual machine to identify situations when the class has been tampered with or innocently changed and incorrectly distributed. This is very evident when class IDs are changed and sent over a network. You will explore several of these scenarios in future articles.

Conclusion

This article covered the basic concept of how classes are dynamically loaded and how this relates to both performance and security. Dynamically loaded classes provide a very substantial performance boost to application development. However, as is often the case, there are some downsides as well; in this case, some security concerns need to be addressed.

It is very helpful to try and create some simple applications and try to produce the results that you covered in this article. As is always the case, experimenting with code firsthand will greatly benefit the learning process. As an added incentive, the lines of code required to illustrate these issues are very small.

In the next article, you will explore how the built-in measures that the compiler and Virtual Machine possess keep security problems for dynamically loaded classes to a minimum.

Reference

www.sun.com

About the Author

Matt Weisfeld is a faculty member at Cuyahoga Community College (Tri-C) in Cleveland, Ohio. Matt is a member of the Information Technology department, teaching programming languages such as C++, Java, C#, and .NET, as well as various web technologies. Prior to joining Tri-C, Matt spent 20 years in the information technology industry gaining experience in software development, project management, business development, corporate training, and part-time teaching. Matt holds an MS in computer science and an MBA in project management. Besides The Object-Oriented Thought Process, which is now in its second edition, Matt has published two other computer books, and more than a dozen articles in magazines and journals such as Dr. Dobb's Journal, The C/C++ Users Journal, Software Development Magazine, Java Report, and the international journal Project Management. Matt has presented at conferences throughout the United States and Canada.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date