Mandatory Access Control at the Object Level in the Java Virtual Machine

For decades, secure operating systems have incorporated mandatory access control (MAC) techniques. Surprisingly, mobile-code platforms such as the Java Virtual Machine (JVM) and the . NET Common Language Runtime (CLR) have largely ignored these advances and have implemented a far weaker security that does not reliably track ownership and access permissions for individual data items. We have implemented a system that adds MAC to an existing JVM at the granularity of objects. Our system maintains a strict separation between mechanism and policy, thereby allowing a wide range of policies to be enforced. In preliminary benchmarks, we find that the run-time overhead of tracking MAC tags for every object is around 30%. 1.INTRODUCTION Three important trends have been recently emerging in the field of trustworthy computing. At the application level, more and more code is now being targeted at high-level language runtimes and virtual machines that execute some form of safe, platform-independent bytecode. The most prevalent examples of this are the Java virtual machine[1], and the more recent . NET common language runtime[2]. Such code platforms offer a number of advantages over native code. The virtual machine performs a number of static and dynamic checks to ensure a basic level of code safety – type-safety, and control flow safety. Type safety ensures that operators and functions are applied only to operands and arguments of the correct types. A special case of type safety is memory safety, which prevents reading and writing to illegal memory locations – for example, beyond the bounds of an array – and thereby also provides separation between different processes without the need for hardware-based memory management. Control flow safety prevents arbitrary jumps in code (say, into the middle of an instruction, or to an unauthorized routine). These basic properties of safe code are enforced by a combination of static (e.g. bytecode verification) and dynamic (e.g. array bounds checks) techniques. Thus, safe code does away with a major source of bugs and vulnerabilities in current systems that stem from unsafe memory operations in C – such as buffer overruns and format string attacks. At the operating system level, there has been a dire need of mechanisms to enforce information flow, confidentiality and integrity. Mandatory access controls, thought only to be relevant to (and originally designed for military systems[3], are gradually beginning to be incorporated into commodity open operating systems such as BSD and Linux. Projects like TrustedBSD[4] and Securityenhanced Linux[5] (SELinux) add techniques and tools to specify, manage and enforce a range of mandatory access controls. As opposed to discretionary access controls that rely on users to specify a security policy, and also do not control access throughout an object's lifetime, mandatory access controls rely on centrally administered policies that are imposed on every data item in the system throughout its lifetime. Examples of systems that need strict information flow controls are payment processing e-business applications, medical data applications, as well as upcoming utility computing “grids”, where computational resources are remotely “rented” out. Control of sensitive information is also becoming increasingly important on personal computers. This is highlighted by the wide spread of various spyware programs that, in addition to whatever it is they purport do to, also have backdoors and surreptitiously leak information from the computer to remote sites. At the hardware level and system software level, the Trusted Computing initiative (headed by the Trusted Computing Group [6]) is working towards introducing a hardware “root of trust” into commodity computing devices such a PCs and PDAs. The solution depends on a combination of minimal trusted hardware and a system software stack that takes advantage of it. The trusted hardware, also called the trusted module, can store secrets and check system integrity. This is combined with a layer of trusted system software that can use the module's functionality to check and enforce the integrity and confidentiality of the rest of the system. These three trends together highlight the increased need for security on commodity personal computers, and in particular, a shift away from discretionary user-controlled mechanisms to stricter, centrally controlled mandatory mechanisms. The Java virtual machine is widely used as a portable and safe development target, but its current security mechanisms are geared towards discretionary controls. These are typically imposed by a user to limit access to system resources by the execution of untrusted code in a Java Virtual Machine. Currently, the JVM lacks mechanisms to either specify or enforce information flow constraints. Information flow is one of the most important security-relevant program properties. Programs can leak information in many and subtle ways. The most obvious way is to publicly display or communicate confidential information. Less direct methods use control flow that depends on confidential data. Even subtler are covert channels that convey information through indirect sideeffects of program execution, such as CPU usage, or energy and voltage fluctuations. Even highly inspected software may have subtle information leaks. Current systems suffer from a dearth of mechanisms to ensure the confidentiality of sensitive data. In most cases, the software vendor is simply trusted to have provided well-behaved software. Due the highly complex and dynamic nature of today's system software, this is far from adequate. What is needed is an automated method to either enforce or explicitly prove the absence of information leaks. As a solution, we have extended the JVM with functionality to do mandatory access control at the granularity of objects. Our implementation strictly separates the enforcement mechanism from the specification of polices. This allows flexible specification and enforcement of a wide range of policies. Moreover, we show that these techniques are implementable in current JVMs with minimal modifications to other JVM subsystems, while maintaining full backwards compatibility. We have implemented this by adding an access control tag to each object, and modifying the virtual machine to check that tag at every data access to an object. Policies will take the form of predicates over these access control tags. Since mechanism and policy are strictly separated, various policies can be plugged in to the VM. A key advantage of keeping mandatory controls in the virtual machine is that it is completely transparent to programs being run in it. No access to source is needed, and the bytecode format does not need to be changed. The binding of code and policy happens at runtime, when mandatory access tags are assigned to objects. The novel contributions of this paper are twofold: to explain the need for mandatory access controls in the Java virtual machine, and present its design and implementation. We also evaluate and discuss the impact of introducing this new access control mechanism into the JVM. Finally, we compare our scheme with existing access control techniques for Java, and discuss the advantages and disadvantages of each. The rest of this paper is structured as follows: Section 2 gives an overview and evaluation of present techniques for access control and information flow in Java, both at the language as well as virtual machine level; Section 3 presents the design and implementation of mandatory access control in a Java virtual machine – this forms the core of the paper; Section 4 evaluates our design against a number of well-known security principles; Section 5 briefly explains the context in which this work was done – semantic remote attestation; Section 6 presents related work in access control; Section 7 discusses avenues for future work; and Section 8 concludes. 2.EXISTING APPROACHES Early Java implementations (up to JDK 1.1) had two distinct security environments. The first environment, a complete sandbox, was meant to constrain the execution of applets downloaded from the Web. These applets were considered completely untrusted. The sandbox disallowed any access to the local filesystem, as well as any network connections to domains other than the one from which the applet originated. This sandbox policy was designed to prevent untrusted code from leaking local data, and consuming too many network resources. The second environment had no constraints at all, and was used to run local code on a machine. Code on the local disk was considered completely trusted. Thus, this early model was essentially all-or-nothing, accounting for either completely untrusted, or completely trusted code, with no gradations in between. Later versions of Java (after JDK 1.2) added capabilities to create more graded security environments, and provide a variety of more fine-grained security permissions[7]. Instead of being trusted (local), or untrusted (remote), code was now associated with principals. A public key infrastructure and cryptographic signatures were used to bind principals to code. A security policy specified what permissions code originating from various principals would get. Permissions included filesystem read and write permissions, and network socket capabilities. Enforcement was relegated to a runtime security manager that regulated access to privileged resources by looking up the permissions possessed by the object that made the request. For example, a policy may specify that all code digitally signed by the domain uci.edu is allowed to read any local file, but to write only under /tmp. However, there are many useful security polices that the current Java architecture does not address. Higher level policies that depend on program state cannot be specified. An example of such a policy is “disallow transmitting on the network after reading from the local filesystem”. Inlin