Redefining java.lang.System with Byte Buddy

The previous post talked about using Java's SecurityManager to prevent attackers from gaining access to sensitive resources. This is complicated by the fact that if the wrong permissions are granted, or a type confusion attack in the JVM is used, it's possible to turn off SecurityManager by calling System.setSecurityManager(null).

There should be a way to tell the JVM that once a SecurityManager is set, it should never be unset. But this isn't in the JVM itself right now, and adding it would mean redefining java.lang.System itself. So let's go do that.

The example project is at https://github.com/wsargent/securityfixer.

The first step is to use the Java Instrumentation API. This will allow us to install a Java agent before the main program starts. In the Java agent, we'll intercept the setSecurityManager method, and throw an exception if the security manager is already set.

The second step is Byte Buddy, a code generation tool that will create new bytecode representing the System class. Byte Buddy comes with an AgentBuilder that can be attached to the instrumentation instance. Byte Buddy uses ASM under the hood, but doesn't require raw manipulation of byte code and class files the way that ASM does – instead, you write interceptors and Byte Buddy will generate the corresponding byte code. From there, an interceptor appended to the bootstrap class path will be loaded before the actual JVM System class.

public class SecurityFixerAgent {

    public static void premain(String arg, Instrumentation inst) {
        install(arg, inst);
    }

    public static void agentmain(String arg, Instrumentation inst) {
        install(arg, inst);
    }

    /**
     * Installs the agent builder to the instrumentation API.
     *
     * @param arg the path to the interceptor JAR file.
     * @param inst instrumentation instance.
     */
    static void install(String arg, Instrumentation inst) {
        appendInterceptorToBootstrap(arg, inst);
        AgentBuilder agentBuilder = createAgentBuilder(inst);
        agentBuilder.installOn(inst);
    }
}

The interceptor class lives in its own package and is relatively simple:

public class MySystemInterceptor {

    private static SecurityManager securityManager;

    public static void setSecurityManager(SecurityManager s) {
        if (securityManager != null) {
            throw new IllegalStateException("SecurityManager cannot be reset!");
        }
        securityManager = s;
    }
}

The interesting bit is the configuration of the AgentBuilder. Byte Buddy is set up out of the box for class transformation and adding new methods and dynamic classes, not redefinition of static methods, so we have to flip a bunch of switches to get the behavior we want:

private static AgentBuilder createAgentBuilder(Instrumentation inst) {
     // Find me a class called "java.lang.System"
     final ElementMatcher.Junction<NamedElement> systemType = ElementMatchers.named("java.lang.System");

     // And then find a method called setSecurityManager and tell MySystemInterceptor to
     // intercept it (the method binding is smart enough to take it from there)
     final AgentBuilder.Transformer transformer =
             (b, typeDescription) -> b.method(ElementMatchers.named("setSecurityManager"))
                     .intercept(MethodDelegation.to(MySystemInterceptor.class));

     // Disable a bunch of stuff and turn on redefine as the only option
     final ByteBuddy byteBuddy = new ByteBuddy().with(Implementation.Context.Disabled.Factory.INSTANCE);
     final AgentBuilder agentBuilder = new AgentBuilder.Default()
             .withByteBuddy(byteBuddy)
             .withInitializationStrategy(AgentBuilder.InitializationStrategy.NoOp.INSTANCE)
             .withRedefinitionStrategy(AgentBuilder.RedefinitionStrategy.REDEFINITION)
             .withTypeStrategy(AgentBuilder.TypeStrategy.Default.REDEFINE)
             .type(systemType)
             .transform(transformer);

     return agentBuilder;
}

Finally, once that's done, we can write a simple test class:

public class Main {

    // We don't want any ACTUAL security here when we turn on the security manager...
    static class SillyPolicy extends Policy {
        @Override
        public boolean implies(ProtectionDomain domain, Permission permission) {
            return true;
        }
    }

    public static void main(String[] args) throws Exception {
        // Programmer turns on security manager...
        Policy.setPolicy(new SillyPolicy());
        System.setSecurityManager(new SecurityManager());

        System.out.println("Security manager is set!");
        try {
            // Attacker tries to turn off security manager...
            System.setSecurityManager(null);

            // Happens on normal circumstances...
            System.err.println("ATTACK SUCCEEDED: Security manager was reset!");
        } catch (IllegalStateException e) {
            // Happens on agent redefinition of java.lang.System
            System.out.println("ATTACK FAILED: " + e.getMessage());
        }
    }

}

And then running it with the agent enabled and pointing to the interceptor will give us the behavior we want:

java -javaagent:agent/target/securityfixer-agent-1.0-SNAPSHOT.jar=bootstrap/target/securityfixer-bootstrap-1.0-SNAPSHOT.jar securityfixer.Main

Gives us the output:

Security manager is set!
ATTACK FAILED: SecurityManager cannot be reset!

The above is an example you can run in a test, but throwing an exception or even an error is not what you ultimately want to do. The system is corrupt and attacker code is being run. What you want to do is kill the JVM immediately and notify the system of intrusion: this is the DJB Way.

In this case, System.exit is not what you want, because shutdown hooks are still run before the JVM exits. What you really want to do is call Runtime.getRuntime().halt(-137) and then use the -XX:OnError= command line flag to dump the core and check the exit status. If it's -137, then you have a confirmed intrusion, and should put your system on high alert.

Note that there are other places that must be locked down, such as Policy.setPolicy, and you should also brainstorm for various other "fixed points" that would cause immediate termination in your security review. You may not be able to stop intrusion, but you'll make it that much harder to avoid detection.

Comments