Java Security Guards Embedded Networks
From the beginning, the Java programming language was designed with security in mind. Yet developers remained suspicious about its secure nature. They were blinded by the novelty of a portable language that could transfer applications from system to system on an as-needed basis. During its initial shakedown period, Java did encounter some relatively minor security problems. At that point, many developers decided—with little real examination of the issues involved—that Java was insecure. It was deemed unsuitable for applications like electronic commerce.
In the PC environment, Java has successfully overcome these early suspicions. This statement is especially true for Web applications. In this arena, developers recognize the language's usefulness. They also understand its built-in security features. In the embedded community, however, the story is quite different. Generally, developers of networked embedded systems are unaware that Java provides multiple security checks. Such measures make it extremely difficult for downloaded rogue software to attack a system.
It's unfortunate that developers continue to hold this attitude. After all, networking is becoming pervasive in embedded systems ranging from set-top boxes to handheld multimedia devices. Meanwhile, the increasingly high performance that's offered by digital signal processors (DSPs) is steadily enabling new forms of communication to these systems.
Multiprocessor architectures provide a solid hardware and software foundation for building mobile systems. Such systems can run a variety of multimedia programs, including Java applications. Texas Instruments' OMAP platform is an example of this type of architecture. It is based on a DSP and enhanced ARM microcontroller.
Until embedded-systems developers understand and make use of the security that's inherent in Java, however, the full potential of these platforms won't be realized. This article defines Java's security mechanisms. To help developers implement their security policies, it also discusses how to employ those mechanisms in software development. The sample code that is shown will assist developers in evaluating Java protections and creating a mechanism for security implementation. Readers can immediately duplicate this code on their workstations, as it is designed to run as an applet within the web browser.
Once they have this information in hand, developers will understand the security that's offered by Java. They'll also be better prepared to build secure Java Virtual Machines (JVMs) for their finished products.
Java offers security at the application level. There, it complements other, more fundamental types of protection. For instance, platforms like OMAP provide security in hardware to prevent boot-code tampering. The Mobile Information Device Profile (MIDP) specification—which is built on top of the Connected Limited Device Configuration (CLDC)—provides a secure run-time environment for Java in mobile information devices. These devices include cellular phones and PDAs. For protection in data transmission, all of the predefined Java applications or classes that are loaded must be properly signed. The signing is done using an X.509 Public Key Infrastructure (PKI) security standard.
These protections—especially MIDP—demonstrate the great importance that embedded-systems developers attach to security. MIDP is the result of a collaborative effort of over 50 companies. Among those companies are mobile-device manufacturers, wireless carriers, and mobile software vendors. Their collaboration resulted in a specification that guarantees end-to-end security for networked applications. At the same time, it leverages capabilities like Secure Socket Layer (SSL) and HTTP using SSL (HTTPS).
The Java programming environment provides mechanisms that ensure security for the execution of Java programs. Security exists both outside and inside of the JVM, which is the target of the Java compiler. All byte code is interpreted inside the JVM as well. Outside of the JVM, language support assists in making Java a more secure programming environment.
This security takes place on three fronts. First of all, there are no pointers in Java-only references. Arithmetic functions can be performed with pointers but not with references. As a result, it's not possible to scan memory. Secondly, changing the cast of an object invokes a check at either compile or run time. Access to methods and fields are checked at run time. If an application tries to access a method of a class that's declared private, a run-time exception (IllegalAccessException) is generated.
A final security feature is that the Java compiler produces an architecture-neutral .class file. This file makes it almost impossible to discover how a program is laid out in memory. As a result, it's very difficult to take advantage of any information about memory maps to devise attacks.
JAVA IN THE SANDBOX While the language protections outside of the JVM are reassuring, they're not accessible for use in development. On the other hand, the security mechanisms within the JVM are accessible. Java is designed so that developers have a great deal of control over how security is implemented within their systems.Inside the JVM, security concerns itself with the execution of byte code—the object code that's created by the compiler. The term "sandbox" refers to the JVM mechanisms that are used in creating a secure environment. In that environment, Java applications can run. Normally, sandbox security is illustrated as shown in Figure 1. The diagram indicates that the code that's loaded from remote sources cannot make direct use of the local resources within the target system.
To be technically accurate, the Kilobyte Virtual Machine (KVM) is used in embedded systems. It is a smaller variant of the JVM. The KVM still supports the sandbox, which is the source of security in any JVM. The KVM received its name because its memory footprint is measured in kilobytes rather than the megabytes required by the Java Virtual Machine.
The default sandbox is comprised of three interrelated parts: the verifier, the class loader, and the security manager. The verifier is the part of the JVM that performs checks on the incoming byte code. This process is transparent to the user. It's not under direct control by the user either. The security manager enforces the security policies for executable content. In other words, it controls the actions that a particular Java class can perform based on some policy. The class loader forges the connection between the JVM and the outside world.
In the early days of Java—around 1996—people often referred to the Java security model as a "three-layer defense." Though such a label is still sometimes encountered in Java-security literature, it's misleading. It implies that if an application penetrates the first layer, two layers are left to set things straight. Actually, the parts are more like links in a chain. If any of the three parts breaks, the entire security system fails.
The following sections discuss the verifier, class loader, and security manager in greater detail. They also make development recommendations based on the interests of greater system security. The work that's presented is taken in part from the research of Dr. Ravindra Rao, co-founder of KiwiLabs. Because the example code is applied to the running of an applet within a web browser, readers can duplicate this work on their workstations. It also can be used in a more general framework of embedded systems in which applets cannot be run.
BYTE-CODE VERIFIER Java automatically checks untrusted code before it is allowed to run. One of its methods of checking, for example, is to verify the class files that contain byte code. Once Java code has been verified, it can execute in an uninterrupted fashion on a JVM or KVM. While the code runs, there's much less need to make security-critical checks. This strategy leads to efficient Java execution. It also offsets the speed concerns that are raised by Java's security checking.The verifier is built into the virtual machine. It cannot be accessed by Java programmers or users. In most implementations, the verifier automatically examines the Java code when it arrives at the VM and is formed into a class by the class loader (FIG. 2).
The verifier checks byte code at a number of different levels. The simplest test makes sure that the format of a code fragment is correct. On a less basic level, a built-in theorem prover is applied to each code fragment. This theorem prover helps to make sure that byte code doesn't forge pointers, violate access restrictions, or access objects using incorrect type information.
If the verifier discovers a problem with a class file, it throws an exception. Loading then ceases and the class file never executes. The verification process—together with the security features that are built into the language and checked at run time—helps to establish a base set of security guarantees.
The verifier also ensures that the class files that refer to each other preserve binary compatibility. Because of Java's ability to dynamically load classes, it's possible that a class file being dynamically linked may not be compatible with a referring class. Binary incompatibility problems could occur when a library of Java classes is updated. Problems also may arise if a class from a large Java program isn't recompiled during development.
The rules of compatibility govern the ability to change the use of classes and methods without breaking binary compatibility. For instance, it's acceptable to add a method to a class that's used by other classes. But it's not acceptable to delete methods from a class that's used by other classes. The verifier enforces the compatibility rules.
Binary incompatibility also has security implications. Picture a scenario in which a malicious programmer can get the virtual machine to accept a set of mutually incompatible classes. The hostile code will probably be able to break out of the sandbox.
Before a Java program can run, it must be loaded into the VM. The loading process begins by reading a file that contains the byte code (i.e., has a .class name). The file is then stored in a buffer. The verifier acts on the information in this buffer. It takes advantage of the fact that Java byte code conforms to a well-known format (8-bit bytes). Plus, each Java .class file contains information about one Java class.
A .class file is a stream of 8-b bytes along with larger quantities that are represented in terms of 8-b bytes. This information is stored in big-endian format. In other words, the most significant bits are leftmost. The first 4 bytes act as an identifier. The other information that's contained refers to the major and minor number of the compiler that produced the .class file. The VM supports classes that originated from a compiler with a fixed major number as well as a minor number that has a predefined upper limit. The constant pool is an array of structures that represent the names of classes, interfaces, fields, and methods. Debugging information also is available.
To execute byte code, verification is clearly part of the larger process that's undertaken by the VM. For instance, suppose a developer wants to execute a program that simply writes "hello world" to a standard output. He or she must take the following steps:
- Load the .class file into the VM.
- Link the byte code with the Java run-time environment.
- Verify the byte code.
- Allocate resources for run time.
- Resolve references.
- Initialize classes.
- Invoke the main method.
Through this whole process, the byte-code verifier's most important accomplishments are that it:
- Checks the format of the .class file.
- Protects against version skew.
- Checks for stack overflow.
- Checks for illegal data conversions.
- Checks instructions to ensure proper parameters on the stack.
To write a Java program, users are required to set an environment variable known as CLASSPATH. This variable is used by the default class loader to load trusted classes. The logic is that if classes are found under CLASSPATH, they must have been put there by the person who set this variable. Subsequently, the default class loader can trust those classes.
If there's a class that isn't listed in the CLASSPATH, a separate class loader must be provided to load it. The implication here is that the class loader is part of the class identity. For instance, browsers often use different class loaders to load classes from varied sources. Given that class loaders play a vital role in the loading of classes, the security manager must check to see if a class is allowed to create a class loader (FIG. 3). Put simply, two classes are of the same type only if they have the same fully qualified name (FQN) and are loaded by the same class loader.
The following statements depict some definitions of class-loader methods. While the bodies for these methods are too lengthy to be included here, they are available at www.ti.com/javasecurity.
The class Java.lang.classloader is an abstract class. From it, other class loaders can be subclassed. It is defined with the statement:
protected abstract class loadClass(String name, Boolean resolve) throws ClassNotFoundException;
The following is an example of a class loader:
import Java.io.*; import Java.net.*; public final class URLClassLoader extends Classloader \{ Extend Java.lang.ClassLoader which is an abstract class private String urlAsString;
The next string contains the location from which the class loader will load files. It could be a URL, such as www.foobar.edu. The location is set only once at the time that this class is instantiated:
protected URLClassLoader() throws MalformedException \{ this(null); \}
The constructor for this class takes no arguments:
public URLClassLoader(String urlStr) throws MalformedURLException \{ if (urlStrnull || urlStr.length()
0) throw MalformedURLException("No url provided."); urlAsString = urlStr; \}
The constructor merely checks for the presence of a string and ensures that it has a non-zero length. If these conditions aren't met, the constructor throws a MalformedURLException:
public synchronized Class loadClass(String name, Boolean resolve) throws Java.lang.ClassNotFound Exception \{ \}
In the following statement, loadClass is the abstract method that must be implemented:
private byte\[\] readClassFile(String classFileName) throws FileNotFoundException, IOException \{ \}
In summary, the class loader does the following:
- It takes a name and produces a class object.
- The class loader subclasses from Java.lang.Classloader (an abstract class).
- It defines method loadClass after extending Classloader.
- It maintains separate namespaces.
To accomplish this goal, the class method System.setSecurityManager() is used. The class Java.lang.SecurityManager is an abstract class. To implement a security policy, the user must extend this class. The following is an illustrative portion of a security manager. The complete code can be found on the previously listed web site:
public class URLMain throws MalformedURLException, ClassNotFoundException \{ SampleSM sem;
The class name that's used here for the security manager is SampleSM. We declare it to be of type SampleSM.
Hashtable clHashtable = new Hashtable(); URLClassLoader urlcl; Class cl; StringBuffer urlSB; StringBuffer classFileSB; int ch; String urlString; String classFileName;
The hash table keeps track of all class loaders and string buffers for reading URLs and class files.
ssm = new SampleSM(true); System.setSecurityManager(ssm); .... \}
The following list summarizes the usage of the security manager:
- Java.lang.SecurityManager sets the security policy.
- For each resource, a check is performed to grant access.
- The security manager controls the creation of class loaders.
- Applications set the security manager once.
- The security manager is defined first and then implemented in code.
Another form of attack was applet-based. In this scenario, a class loader was installed from an applet. In turn, the class loader installed Java classes as trusted classes. This attack was made possible by a bug in the Java byte-code verifier. To reduce developers' security concerns, Sun Microsystems (www.sun.com) addressed these problems in later versions of Java.
In the security community, there's a routine joke that there's only one really secure machine. It's a machine that's enclosed in a room with no entry capability, no network connectivity, and no console. Sometimes, the joke also includes the requirement for no power.
These demands are obviously a little too stringent for the real world, in which Java is growing in popularity. Fortunately, early Java developers were very security conscious. They included protections for both language and code execution. The byte-code verifier, class loader, and security manager within the JVM all work in a cooperative manner to strengthen the security features of Java. Moreover, these features are accessible to developers. They allow them to design JVMs that meet specific security requirements for their own systems. The many protections that Java offers make it very unlikely that applications will be able to throw dirt in the proverbial sandbox.