Java Applet Instrumentation

Java 6 has a neat feature that has received little attention: the Attach API

Using this API, you can hook your own code into a process already running on the JVM. It’s meant to build code-profilers and things of that sort, but with a little fooling around, you can attach to a running Applet and play with the live objects. I’ll describe my method – if you know a better way to do this, please let me know.

First, you need to turn off Applet Security. I’m lazy and went with a shotgun approach. Create a new .policy file:

grant { permission java.security.AllPermission; };

Then, set the browser plugin to use this policy. On Windows, try this:

  1. Open the Control Panel
  2. [doubleclick] Java Control Panel
  3. [click] Java tab
  4. [click] Java Applet Runtime Settings
  5. [click] View
  6. [click] Java Runtime Parameters
  7. [type] -Djava.security.policy=C:path_toyour.policy
  8. [click] OK

Same thing for Linux, just figure out where your Applet Runtime settings are.

Now you need a way to run your Agent. Annoyingly, the Agent must live in a jar file. You can build it on the command line using JAR, but I chose to do it programmatically to avoid the extra step. This program takes a single command line argument, the PID (process id) of the JVM the applet is running on. There’s different ways to find it. On Windows, try running “tasklist”. On Linux, “ps -A”. Place this file at “com/stuff/Runner.java”:

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import com.sun.tools.attach.VirtualMachine;
public class Runner {
  public static void main(String args[]) throws Exception {
    //create .jar including all .class files under directory: com/stuff/
    List<String> jarFiles = new ArrayList<String>();
    for(String file : (new File("com/stuff")).list())
      if(file.endsWith(".class"))
        jarFiles.add("com/stuff/"+file);
    String[] filenames = jarFiles.toArray(new String[]{});
    String jarFile = System.getProperty("user.dir")+"/agent.jar";
    JarUtil.jar(filenames, jarFile);
    //find PID of process to monitor
    String pid = args[0];
    //attach agent.jar 
    if(Integer.parseInt(pid) > 0) {
      VirtualMachine vm = VirtualMachine.attach(pid);
      vm.loadAgent(jarFile, null);
    } else {
      System.out.println("Bad PID: " + pid);
    }
  }
}

Rad, so now for the Agent itself. When the JVM calls our Agent, it
passes an instance of Instrumentation, which has the interesting method
getAllLoadedClasses(). A Class isn’t very useful unless there are
static methods to get instances of the Class. Fortunately,
AppletPanelCache.getAppletPanels() comes to the rescue as a way to get
at Objects instead of Classes. Here’s MyAgent.java:

import java.lang.instrument.Instrumentation;
public class MyAgent {
  public static void agentmain(String agentArgs, Instrumentation inst) {
    for(Class klass : inst.getAllLoadedClasses()) {
      if(klass.getName().endsWith("AppletPanelCache")) {
        Method m = klass.getMethod("getAppletPanels", new Class[]{});
        Object[] panels = (Object[])m.invoke(null, new Object[]{});
        for(Object panel : panels) {
        //do something interesting with an instance of Panel 
        }
      }
    }
  }
}

What can you do with a Panel? Well, let’s see, for an instance to be of any use in an Applet, it is likely connected *somehow* to the rest of those top level Panels where everything is shown. With a heavy dose of Reflection, you can recursively explore each Panel’s children, ultimately getting a reference to most (is it most or all? does anyone know?) of the live objects. Once you have a live object, you can do whatever you want – call methods, inspect fields, etc. The methods getComponents() and getWindows() are a good place to start.

So, how to run it? Depends where you have Java installed. Make sure tools.jar is on your classpath. I use this invocation:

  1. compile javac -classpath /your/path/to/tools.jar:. com.stuff.*
  2. run (first, open an applet in your webbrowser)
  3. java -classpath /your/path/to/tools.jar:. com.stuff.Runner -Djava.security.policy=/home/path_to/your.policy

Annoyances – When an Agent calls System.out.println(), it gets the Applet’s PrintStream instead of printing to the Console. My solution? Well, Runner *is* still connected to the Console, so have Agent send all its output to Runner over a socket. There may be a better way. Also, I’ve been unable to attach twice without shutting down and restarting Firefox – not sure exactly why.

One more neat thing you might try – hooking your own AWTEventListener into the AWT Event Processing pipeline:

if(klass.getName().endsWith(Toolkit)) {
  long mask = Long.MAX_VALUE; //call our listener on ALL event types 
  Method m = klass.getMethod("getDefaultToolkit", new Class[]{});
  Toolkit toolkit = (Toolkit)m.invoke(null, new Object[]{});
  Method n = klass.getMethod("addAWTEventListener", new Class[]{AWTEventListener.class, long.class});
  n.invoke(toolkit, new Object[]{new MyAWTEventListener(), mask});
}

One cute idea – dispatch MouseEvent and KeyEvent to make a video game “play” itself.

Remember to reset your Java Applet Runtime Settings, if you are concerned about such things.

References: Hotpatching a Java 6 Application by Jack Shirazi

Tags:

Leave a Reply

Please Note: I've been getting slammed by comment-spam lately, so you might have better luck e-mailing directly if you would like a prompt response. My username is craiget and I use gmail.