Mobile-Code Security Mechanisms for Jini
The Java Language has proven useful for a variety of purposes, including system development and the addition of active content to web pages. To protect against execution of erroneous or potentially malicious code, the Java Virtual Machine verifies code properties before execution and performs additional checks at run time. However, these tests will not protect against many forms of undesirable run-time behavior. Moreover, users cannot easily customize the tests that are performed since these are built into the Java Virtual Machine.
Stanford has previously developed methods for enforcing applet properties, in a manner that may be customized easily. The main technique is bytecode modification. Specifically, we have implemented bytecode filters that analyze the content of bytecode files and insert additional bytecode instructions. These additional instructions may monitor and control resource usage, limit applet functionality, or provide control over inaccessible objects. The Java bytecode modification techniques fall into two classes: class-level modification and method-level modification. In class-level modification, references to one class are modified to refer to another class. This may be done in a way that preserves the Java bytecode typing requirements by using class inheritance. Specifically, the modified class may be declared as a subclass of the unmodified class. Class-level modification is simple and fast, but cannot be applied to final classes since the Java language definition prohibits subclasses of final classes. In these cases, method-level modification is used since it may be applied on a method-by-method basis without regard to class hierarchy restrictions.
The Stanford bytecode filter is written in the Java programming language. The filter is general-purpose, reading a modification specification as input. The modification specification lists classes and methods and their replacements, making the filter generally applicable to a variety of situations. The filter is based on a publicly available library, called JavaClass, that is designed for manipulating Java bytecode files.
As described above, a Jini client locates and installs a service through a lookup server. The protocol is implementation specific, meaning that the Jini specification only requires a protocol based on TCP and UDP transport and object serialization. In particular, the specification does not refer to the RMI registry or wire protocols. The only requirement is that Jini interfaces preserve the RMI remote interface semantics, which means that the client needs to declare a RemoteException on its methods. After the lookup server is found, the lookup server sends to the client an object implementing the net.jini.core.lookup.ServiceRegistrar interface. This is done in the unicast discovery protocol. This appears to be the only specified use of RMI in current Jini. This complicates the problem of identifying a well-known, constant path of remote-class delivery rather difficult.
In general, there are three places where one could intercept the service object class bytecode – during transport from the LookupServer to the lookup service proxy, within the ServiceRegistrar before the serialized object is reconstructed, and within the ServiceRegistrar after object reconstruction.
Since it appears infeasible to intercept the service object along the network path, we decided to intercept the object bytecode in the ServiceRegistrar. This is best done before the serialized object is reconstructed, since modifying the bytecode of an object after reconstruction presents problems. (For example, it is difficult to maintain the state of the object.)
In Java, in order to load a class into the JVM execution, one needs a java.lang.ClassLoader object which does the actual loading via the protected final defineClass method. The whole loading process proceeds as follows. If the class name is referenced implicitly during the code execution, the JVM tries to locate in its internal runtime table a class with such name, that, was loaded by the same ClassLoader which loaded the class making the reference. If there is no such class, the JVM calls the method loadClass( className ) on the ClassLoader which loaded the class making the reference. This method first tries to delegate the loading of the class to its parent ClassLoader by calling its loadClass(), and so forth, up until the bootstrap. If all parents fail to locate the class (not previously loaded by them, and unable to find its .class file), then loadClass() calls findClass() in the ClassLoader that initiated the loading process. If findClass finds the .class file, it loads it into a byte array, and then calls the final, protected method defineClass. If findClass fails to locate the class, it throws a ClassNotFoundException.
In the Java API, there is only one network-aware ClassLoader, java.net.URLClassLoader. Even the java.rmi.server.RMIClassLoader class which implements the RMI-related class loading relies internally on a subclass of the URLClassLoader, sun.rmi.server.LoaderHandler$Loader.
This leads us to two possible ways of intercepting service objects. The first one is to assume that the classes for the downloaded service objects will always be loaded into the JVM by using either the URLClassLoader or the RMIClassLoader, and then replace all references to java.net.URLClassLoader with our extension of it, jinifilter.SafeURLClassLoader. The second one is to replace all calls to java.lang.ClassLoader.defineClass() with the static jinifilter.SafeClassLoader.defineClass(). In the current implementation, we chose assume that the classes for the downloaded service objects will always be loaded into the JVM by using either the URLClassLoader or the RMIClassLoader.
In summary, the architecture for filtering Java bytecode and replacing classes with safe library classes is illustrated by the following diagram:
The client class loader is modified so that Jini proxies installed in the client are scanned before they are executed. For example, a service proxy is defined as part of a service, and shown here as a square box with label “S” inside the service site. When the service joins the lookup service, the service proxy is sent to the lookup service. After discovery, in which the client selects this service, the service proxy is sent (in the form of a serialized service object) to the client. After the client receives the service proxy, the modified client class loader invokes the Java bytecode filter. The filter then modifies classes and methods in the service proxy so that safe classes from the safe class library are used in place of standard classes. This allows us to trace file access, trace network access, prevent denial of service attacks by limiting use of specified resources, and so on. In order to monitor network communication by a proxy, we must have a safe class that performs this monitoring. However, we have found it straightforward to construct safe classes for this and other purposes.
Further technical information about Java bytecode filtering and its application to Jini security is described in the attached conference paper prepared for DISCEX 2001.