Hacking Java libraries

Learn how to replace or patch application classes

Just about every developer at some point has used a library or a component developed by someone else. Just about every developer at some point has also gotten frustrated with the library to the point of being willing to find the guy who decided to make a method private and talk some sense into him. Well, most of us wouldn't go that far, but it would certainly be nice to be able to change things that make our lives miserable. It's not that libraries are written by mean people; it's just that even the brightest designers are unable to foresee all the possible ways that other developers would want to use their code.

Certainly, it is always better to resolve matters peacefully. If you can get the vendor to change his code, or if you are in a position to do it yourself, then certainly do so. But, in real life, the conventional approach does not always work. And this is where things get interesting. Having said that, when should you resort to replacing and patching classes? The following are several of the situations that call for a hacker approach:

  • You are using a third-party library that has the capability you need, but it is not exposed through a public API: For example, until J2SE 1.4, Java Swing did not provide a method to obtain a list of JComponent listeners. The component would store the listeners in a package-visible variable with no public access to it, so there was no way to find out programmatically whether a component had event listeners.
  • You are using a third-party class or an interface, but the functionality exposed is not flexible enough for your application: A simple change in the API can save you days of work or might be the only solution to your problem. In this case, you are happy with 99 percent of the library, but the remaining 1 percent prevents you from being able to use it effectively.
  • There is a bug in the product or API you are using and you cannot wait for the vendor to fix it: JRun 3.0, for instance, had a bug in the JVM version detection on HP UX. While parsing the version string reported by the Java Runtime Environment, it would erroneously conclude that it was running under an older version of the JDK and refuse to run.
  • You need a very close integration with a product, but its architecture is not open enough to satisfy your requirements: Many frameworks separate interfaces from implementation. Internally, interfaces are used to access functionality, and concrete classes are instantiated to provide the implementation. Java core libraries for the most part allow specifying implementation classes through system properties. This is the case for AWT (Abstract Windowing Toolkit) and SAX (Simple API for XML) parser, where implementation classes can be specified using the java.awt.toolkit and org.xml.sax.driver system properties, respectively. Hacking would be required if you needed to provide a different implementation class for a library that does not provide means of customization.
  • You are using third-party code, but the expected functionality is not working: You are not sure whether it is because you are not using it correctly or because of a bug in the code. The documentation does not refer to the problem, and you do not have a workaround. Temporarily inserting debug traces and messages into the third-party code can help you understand what is happening in the system.
  • You have an urgent production issue that has to be fixed: You also cannot afford to go through risky redeployment of the new code to the production environment. The solution to the problem requires a small change in the code that affects only a few classes.

If dealing with third-party code, you might be violating the license agreement, so be sure to read it and run it by your legal department to be safe. Copyright laws can be strictly enforced, and changing third-party code is often illegal. Get the vendor's permission to implement a solution rather than assuming responsibility for the hack. The good news is that by using the method presented in this chapter, you aren't making direct changes to the library or the product you are using. You aren't tampering with the code, but rather providing replacement functionality for the one you are not happy with. In a way, it is like deriving your class from the vendor's class to override a method, although this can be a slippery slope. Legal issues aside, let's see how you can go about doing this.

Read the sidebar "Stories from the Trenches" to see how AT&T fixed a bug in one of their programs.

Finding the class that has to be patched

First, you have to determine what code has to be patched. Sometimes it's fairly obvious and you will know the specific class or interface right away. If you feel you are too smart to be told how to locate the code, then by all means, skip to the section that talks about how to patch. Otherwise, sit back, relax, and learn several approaches to achieving the result.

The general approach

The general method of locating a class to be patched consists of finding a starting point and navigating the execution sequence until you get to the code you want to change. If you do not encounter the code you want to change in the vicinity of the starting point, you must obtain a new starting point and repeat the process. A good starting point is crucial for quick results. Sometimes picking a class to start is fairly obvious. For example, for API or logic, patching the entry point would be the interface or class you want to change. If you want to make a private method of a class into a public method, the starting point is the class in question. If you need to fix a bug that results in a Java exception, the starting point is the class at the top of the stack trace.

Searching for text strings

A large, sophisticated system has dozens of packages and hundreds of classes. If you don't have a clear starting point, you can easily get lost while trying to traverse the application logic. Think about the startup code for an application server such as WebLogic. During startup, WebLogic performs hundreds of tasks and uses many threads to accomplish them—and even with an unlimited supply of caffeine, I would not advise you to try to crack it by traversing from the weblogic.Server class.

The most reliable approach for such cases is a text-based search for a string that is known to be close to the target class. Well-written products and libraries can be configured to produce extensive debug information into a log file. Besides the obvious benefits for maintenance and troubleshooting, this makes locating the code responsible for the functionality in question easier. When you configure the application to write a detailed log of the execution sequence and a problem occurs somewhere, you can use the last successful (or the first erroneous) log message to identify the entry point.

As you might know, the bytecode stores strings as plain text, which means you can search through all .class files for a substring that you have seen in a log file. Suppose while using the security framework, an exception with the text Invalid username is thrown on certain names. The reason for rejection is unknown and so is the solution. The easiest way to get to the code if the stack trace is unavailable is by searching for Invalid username in all the .class files of the framework. Most likely, it will be one or two instances in the entire code, and by decompiling the classfile, you will be able to understand the root of the problem. Likewise, you can search all the classfiles for a method or class name, a GUI label, a substring of an HTML page, or any other string you think is embedded in the Java code.

Working with obfuscated code

A worse scenario is when you have to deal with the obfuscated code. A good obfuscator renames packages, classes, methods, and variables. The best products on the market even encode Java strings, so searching for a trace message can yield no results. This turns your task into a hellish toil of understanding the application piece by piece. Here you have to use a more creative approach; otherwise, it is like trying to find a needle in a haystack. Knowing the principles of obfuscation can help you in the navigation. Although the obfuscator has the freedom to change the application class and method names, it cannot do so for system classes.

For example, if a library checks for the presence of a file and throws an exception if the file is not there, doing a binary search on the exception string might yield no results if the obfuscator was smart enough to encode it. However, doing a search on File or FileInputStream can lead you to the related code. Similarly, if the application incorrectly reads the system date or time, you can search for the java.util.Date or getTime() method of the Calendar class. The biggest problem is that obfuscated classes cannot always be recompiled after decompilation.

A sample scenario that requires patching

We are going to modify the Chat application to show the username and hostname instead of just the hostname in the conversation window. The original application displays the hostname followed by a colon for each message that is received, as shown in Figure 1.

Figure 1. The main window of the original chat

This makes the implementation of the utility easy, but users will certainly prefer to see from which person they are getting messages, rather than which computer is used to send the messages. Chat is free and open for enhancements, but no source code exists for it.

As is common with Java applications, the bytecode is shipped in one or several jar files, so the first task is to create a working directory and unjar all the libraries into it. This allows easy navigation and direct access to the .class files, which are the target of our research. After creating a working directory executing jar xf chat.jar, we see the following files:


Let's try all the approaches of locating the starting point presented earlier and see which one works best for this application.

Using the class name

Luckily, the bytecode is not obfuscated, so we can look at the class names and see whether we can pick the winner. A 5-second examination should lead to the conclusion that MainFrame is the best candidate for a first look. Browsing through the decompiled code, we see that all recording of the conversation is done via the appendMessage() method that looks like this:

    void appendMessage(String message, MessageInfo messageInfo) {
        if (messageInfo == null) {
            this.conversation.append("<font color=\"red\">");
        else {
            this.conversation.append("<font color=\"blue\">");
        this.conversation.append(": ");
        this.txtConversation.setText(this.conversation.toString() +

The implementation of the method uses the getDisplayName() method of the MessageInfo class to obtain the name of the sender. This leads us to decompiling the MessageInfo class to obtain the implementation of getDisplayName(), shown here:

    public String getDisplayName() {
        return getHostName();

Bingo! We have found out that the Chat user interface relies on MessageInfo and that the current implementation uses just the hostname. Our task is therefore to patch MessageInfo.getDisplayName() to use both the hostname and username.

Searching for text strings

Let's pretend that Chat is a large application with more than 500 classes in many different packages. Hoping to guess the right class based on its name is like hoping your code will run correctly after the first compile. You need to use a more reliable method to obtain a starting point. The Chat utility writes pretty decent log messages, so let's try to use it. After starting it, we send a message to another user, get a reply, and get the following output on the Java console:

Initializing the chat server... 
Trying to get the registry on port 1149 
Registry was not running, trying to create one... 
ChatApplication server initialized 
Sending message to host JAMAICA: test 
Received message from host JAMAICA

It is not hard to guess that appending a new message to the conversation history occurs when a message is sent or received. It is also fairly obvious that information such as the host that sent, or was a destination for, a message would not be a part of a static string. Therefore, we will use Received message from host as a search criteria for all the .class files in the working directory. The search produces one file, ChatServer.class, which we promptly decompile to get ChatServer.jad. Searching for the string inside the decompiled source code leads us to the receiveMessage() method, which is as follows:

public void receiveMessage(String message, MessageInfo messageInfo)
    throws RemoteException
    System.out.println("Received message from host " + messageInfo.getHostName());
    if(messageListener != null)
        messageListener.messageReceived(message, messageInfo);
1 2 Page 1
Page 1 of 2