Securing Java Code: Part 3
Protecting Java Code in Memory
Once Java code has been run on a machine, there is very little that protects it from being compromised. Developers need to assume that eventually their source code will be decompiled and scrutinized. There are, however, ways to sanitize source code after a program runs, and steps to make it expensive, time consuming, and extremely difficult to unravel.
Step 1: Memory Management
Java security does not specify which runtime data areas are laid out inside the Java Virtual Machine. If you look into a class file, you will not see any memory addresses. The JVM decides where in its internal memory to put the bytecode and other data it parses from the class file when it loads one. When it starts a thread it decides where to put the Java stack. When it creates a new object, it decides where in memory to put it.
This is helpful to security because one cannot predict by looking at a class file where the data representing a class, or objects instantiated from that class, will be kept in memory.
The JVM implementation also differs for each vendor. The designers of each JVM decide which data structures their implementation will use to represent runtime data areas, and where in memory they will be placed. This isn't something that needs to be enforced at runtime; it is a built-in feature of the Java language.
Despite the benefits, there are also pitfalls in Java's structure that make protecting source code difficult. For instance, native methods don't go through the Java security API. Since the security manager isn't checked, once a thread goes native, the security doesn't apply.
Garbage collection in Java can also be problematic. In some Java implementations, the garbage collector may copy sensitive memory while it is in use for efficiency, and objects created by a running program are sometimes collected in the garbage heap.
The easiest way to protect source code is to not allow the code to be stored magnetically in memory. Sensitive data in memory (like passwords and cryptographic keys) should be in memory for as short a time as possible and should never be written to disk if at all possible.
Applications have a responsibility to be reasonably secure. When this data is stored, there should be some preventative measures taken so it cannot be recovered: such as encryption.
Try to prevent programs from making memory dumps when they crash. Memory dumps are usually stored on the local file system, which may make them easily accessible.
Step 2: Erasing
Erasing data from memory may be more difficult than one might think. Deleting usually only means removing a system file entry that points to a file. The file still exists somewhere, until it gets overwritten. Even when overwritten, files can still be recovered, given the right equipment, software and knowledge.
Some software utilizes overwriting schemes to try to remove files from magnetic memory. The software does this by overwriting files multiple times, alternating overwriting with binary ones, zeroes, patterns of ones and zeroes, and random data. The concept behind this kind of overwriting scheme is to flip each magnetic domain on a disk back and forth as much as possible, without writing the same pattern twice in a row. Basically each track of memory contains an image of everything ever written to it, but each layer gets smaller and harder to retrieve the further back it was made.
No one really knows how many passes are sufficient. To avoid the risk, try not to write to the disk at all. Avoid putting sensitive data in a file on the file system. If one has to store sensitive data on the file system for any length of time, it should be encrypted. After using any of the sensitive data, erase it immediately by writing over the actual data.
Overwriting in Java is problematic because Java's other features make it difficult to overwrite where the data was actually stored. Java will normally create new structures that live elsewhere in memory. The solution is to use mutable data structures that allow one to dynamically replace elements. You should keep track of these data structure and then write over the whole length of the data to erase it from memory.
Step 3: Encryption
Try to avoid saving sensitive data. When keeping a password log in a database, use a cryptographic hash so that the application never has to store the actual password.
Sometimes sensitive data needs to be stored on the hard disk without protecting it. An application may need to view a sensitive document that is too big to fit into memory all at once, or encryption may cause unacceptable performance degradation. In this case, try to protect the file while it is in use, and then delete it from local memory as quickly as possible.
The next part of this series will discuss preventative measures for decompilers.
References and Resources
- Java 2 Network Security, Second Edition, Pistoia, Reller, Gupta, Nagnur, and Ramini, Prentice Hall, 1999.
- Java Security Handbook, Jamie Jaworski and Paul Perrone, SAMS Publishing, 2000.
- Securing Java, Gary McGraw and Ed Felton, John Wiley & Sons, Inc., 1999.
- Princeton University's Secure Internet Programming Team.
About the Author
Thomas Gutschmidt is a freelance writer, in Bellevue, Wash., who also works for Widevine Technologies.