Java Applet Instrumentation

(2009)

Please Note: this doesn't seem to work anymore. I haven't gotten around to figuring out why.. cheating at Yahoo Games was fun while it lasted ;)

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 jarFiles = new ArrayList();
   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:

compile javac -classpath /your/path/to/tools.jar:. com.stuff.*

# run (first, open an applet in your webbrowser)

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