diff --git a/README.md b/README.md index c5fea6e..3059f9e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ### ... and this library makes it suck a little less. -supports java 9 to 19 +supports java 9 to 21 Including: @@ -13,5 +13,3 @@ Including: 2. Change values of fields, invoke methods and create new instances by invoking constructors. 3. Support for accessing "Inaccessible" objects which would result in an InaccessibleObjectException when accessing with regular reflection. - -4. Method to crash the JVM (lol) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ae04661..a595206 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/reflection/ConstructorReflection.java b/src/main/java/reflection/ConstructorReflection.java new file mode 100644 index 0000000..d2f9f73 --- /dev/null +++ b/src/main/java/reflection/ConstructorReflection.java @@ -0,0 +1,98 @@ +package reflection; + +import java.lang.reflect.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import static reflection.ReflectionUtils.*; + +public class ConstructorReflection { + + private static final Method copyConstructor; + private static final Method getDeclaredConstructors0; + + + static { + try { + copyConstructor = forceAccessible(Constructor.class.getDeclaredMethod("copy"), true); + getDeclaredConstructors0 = forceAccessible(Class.class.getDeclaredMethod("getDeclaredConstructors0", boolean.class), true); + } catch (NoSuchMethodException e) { + throw new RuntimeException("Initialization of ConstructorReflection failed", e); + } + } + + /** + * creates an instance of a class using the default constructor (even when the default constructor does not exist) + * @param clazz the class to make an instance of + * @return an instance of the class + */ + public static T createInstanceWithoutConstructor(Class clazz) { + try { + return (T) unsafe.allocateInstance(clazz); + } catch (InstantiationException e) { + e.printStackTrace(); + } + return null; + } + + /** + * makes a new instance of the constructor passed in + * @param c constructor to create new instance of + * @param args the arguments to make a new instance with + * @return new instance of constructor + */ + public static T useConstructor(Constructor c, Object ... args) { + try { + if(args.length == c.getParameterCount()) { + boolean isOverride = isAccessible(c); + + forceAccessible(c, true); + + T instance = c.newInstance(args); + + forceAccessible(c, isOverride); + + return instance; + } + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + e.printStackTrace(); + } + return null; + } + + /** + * gets constructor + * @param clazz class to get constructor of + * @param classes the argument types of the constructor + * @return the constructor + */ + public static Constructor getConstructor(Class clazz, Class ... classes) throws NoSuchMethodException { + for(Constructor constructor : getConstructors(clazz)) { + if (Arrays.equals(constructor.getParameterTypes(), classes)) { + return constructor; + } + } + throw new NoSuchMethodException(methodToString(clazz, "", classes)); + } + + /** + * gets all fields from class and all superclasses if specified + * @param clazz the class the field is in + * @return the fields specified + */ + @SuppressWarnings("unchecked") + public static Constructor[] getConstructors(Class clazz) { + try { + List> constructors = new ArrayList<>(); + + for (Constructor constructor : (Constructor[]) getDeclaredConstructors0.invoke(clazz, false)) { + constructors.add((Constructor) copyConstructor.invoke(constructor)); + } + + return constructors.toArray(new Constructor[0]); + } catch (InvocationTargetException | IllegalAccessException e) { + e.printStackTrace(); + } + return new Constructor[0]; + } +} diff --git a/src/main/java/reflection/FieldReflection.java b/src/main/java/reflection/FieldReflection.java new file mode 100644 index 0000000..0ed2637 --- /dev/null +++ b/src/main/java/reflection/FieldReflection.java @@ -0,0 +1,140 @@ +package reflection; + +import java.lang.reflect.*; +import java.util.ArrayList; +import java.util.List; + +import static reflection.ReflectionUtils.*; + +public class FieldReflection { + + private static final Method getDeclaredFields0; + private static final Method copyField; + + static { + try { + getDeclaredFields0 = forceAccessible(Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class), true); + copyField = forceAccessible(Field.class.getDeclaredMethod("copy"), true); + } catch (NoSuchMethodException e) { + throw new RuntimeException("Initialization of FieldReflection failed", e); + } + } + + public static void setStaticFieldValue(Field f, Object value) { + setFieldValue(f, null, value); + } + + /** + * sets field's value (doesn't work with trusted final fields) + * @param f field to change + * @param instance instance to change the field in (null if field is static) + * @param value the value to change the field to + */ + public static void setFieldValue(Field f, Object instance, Object value) { + long offset; + Object obj; + + if(instance == null) { + offset = getStaticFieldOffset(f); + obj = f.getDeclaringClass(); + } else { + offset = getObjectFieldOffset(f); + obj = instance; + } + + switch (f.getType().getName()) { + case "int" -> unsafe.putInt(obj, offset, (Integer) value); + case "boolean" -> unsafe.putBoolean(obj, offset, (Boolean) value); + case "byte" -> unsafe.putByte(obj, offset, (Byte) value); + case "long" -> unsafe.putLong(obj, offset, (Long) value); + case "short" -> unsafe.putShort(obj, offset, (Short) value); + case "float" -> unsafe.putFloat(obj, offset, (Float) value); + case "char" -> unsafe.putChar(obj, offset, (Character) value); + case "double" -> unsafe.putDouble(obj, offset, (Double) value); + default -> unsafe.putObject(obj, offset, value); + } + } + + public static Object getStaticFieldValue(Field f) { + return getFieldValue(f, null); + } + + /** + * gets field's value + * @param f field to get value of + * @param instance instance to get field value of (null if field is static) + * @return value of the field in the instance + */ + public static Object getFieldValue(Field f, Object instance) { + long offset; + Object obj; + + if(instance == null) { + offset = getStaticFieldOffset(f); + obj = f.getDeclaringClass(); + } else { + offset = getObjectFieldOffset(f); + obj = instance; + } + + return switch (f.getType().getName()) { + case "int" -> unsafe.getInt(obj, offset); + case "boolean" -> unsafe.getBoolean(obj, offset); + case "byte" -> unsafe.getByte(obj, offset); + case "long" -> unsafe.getLong(obj, offset); + case "short" -> unsafe.getShort(obj, offset); + case "float" -> unsafe.getFloat(obj, offset); + case "char" -> unsafe.getChar(obj, offset); + case "double" -> unsafe.getDouble(obj, offset); + default -> unsafe.getObject(obj, offset); + }; + } + + public static Field getField(Class clazz, String name) throws NoSuchFieldException { + return getField(clazz, name, false); + } + + /** + * gets field from class and all superclasses + * @param clazz the class the field is in + * @param name the name of the field + * @param includeInheritedFields if to search in the superclasses + * @return the field + */ + public static Field getField(Class clazz, String name, boolean includeInheritedFields) throws NoSuchFieldException { + for(Field field : getFields(clazz, includeInheritedFields)) { + if (field.getName().equals(name)) { + return field; + } + } + throw new NoSuchFieldException(name); + } + + /** + * gets all fields from class and all superclasses if specified + * @param clazz the class the field is in + * @param includeInheritedFields if to get fields from superclasses + * @return the fields specified + */ + public static Field[] getFields(Class clazz, boolean includeInheritedFields) { + try { + List fields = new ArrayList<>(); + + while (clazz != null) { + for (Field field : (Field[]) getDeclaredFields0.invoke(clazz, false)) { + fields.add((Field) copyField.invoke(field)); + } + if(includeInheritedFields) { + clazz = clazz.getSuperclass(); + } else { + clazz = null; + } + } + + return fields.toArray(new Field[0]); + } catch (InvocationTargetException | IllegalAccessException e) { + e.printStackTrace(); + } + return new Field[0]; + } +} diff --git a/src/main/java/reflection/MethodReflection.java b/src/main/java/reflection/MethodReflection.java new file mode 100644 index 0000000..1dac2b5 --- /dev/null +++ b/src/main/java/reflection/MethodReflection.java @@ -0,0 +1,103 @@ +package reflection; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static reflection.ReflectionUtils.*; + +public class MethodReflection { + + private static final Method getDeclaredMethods0; + private static final Method copyMethod; + + static { + try { + getDeclaredMethods0 = forceAccessible(Class.class.getDeclaredMethod("getDeclaredMethods0", boolean.class), true); + copyMethod = forceAccessible(Method.class.getDeclaredMethod("copy"), true); + } catch (NoSuchMethodException e) { + throw new RuntimeException("Initialization of MethodReflection failed", e); + } + } + + public static Object useStaticMethod(Method m, Object ... args) { + return useMethod(m, null, args); + } + + /** + * invokes the method passed in + * @param m method to invoke + * @param instance the instance of the object to invoke the method with (null if method is static) + * @param args the arguments to invoke the method with + * @return new instance of constructor + */ + public static Object useMethod(Method m, Object instance, Object ... args) { + try { + if(args.length == m.getParameterCount()) { + boolean isOverride = isAccessible(m); + + forceAccessible(m, true); + + Object value = m.invoke(instance ,args); + + forceAccessible(m, isOverride); + + return value; + } + } catch (InvocationTargetException | IllegalAccessException e) { + e.printStackTrace(); + } + return null; + } + + public static Method getMethod(Class clazz, String name, Class ... classes) throws NoSuchMethodException { + return getMethod(clazz, name, false, classes); + } + + /** + * gets method from class and all superclasses + * @param clazz class the method is in + * @param name the name of the method + * @param includeInheritedMethods if to search in the superclasses + * @param classes the argument types of the method + * @return the constructor + */ + public static Method getMethod(Class clazz, String name, boolean includeInheritedMethods, Class ... classes) throws NoSuchMethodException { + for(Method method : getMethods(clazz, includeInheritedMethods)) { + if (method.getName().equals(name) && Arrays.equals(method.getParameterTypes(), classes)) { + return method; + } + } + throw new NoSuchMethodException(methodToString(clazz, name, classes)); + } + + /** + * gets all methods from class and all superclasses if specified + * @param clazz the class the method is in + * @param includeInheritedFields if to get methods from superclasses + * @return the methods specified + */ + public static Method[] getMethods(Class clazz, boolean includeInheritedFields) { + try { + List methods = new ArrayList<>(); + + while (clazz != null) { + for (Method method : (Method[]) getDeclaredMethods0.invoke(clazz, false)) { + methods.add((Method) copyMethod.invoke(method)); + } + if(includeInheritedFields) { + clazz = clazz.getSuperclass(); + } else { + clazz = null; + } + } + + return methods.toArray(new Method[0]); + } catch (InvocationTargetException | IllegalAccessException | IndexOutOfBoundsException e) { + e.printStackTrace(); + } + return new Method[0]; + } +} diff --git a/src/main/java/reflection/ReflectionUtils.java b/src/main/java/reflection/ReflectionUtils.java new file mode 100644 index 0000000..dae283d --- /dev/null +++ b/src/main/java/reflection/ReflectionUtils.java @@ -0,0 +1,74 @@ +package reflection; + +import sun.misc.Unsafe; + +import java.lang.reflect.*; +import java.util.Arrays; +import java.util.stream.Collectors; + +import static reflection.FieldReflection.*; + +public class ReflectionUtils { + + static final Unsafe unsafe; + private final static Object theInternalUnsafe; + private final static Method objectFieldOffset0; + private final static Method staticFieldOffset0; + + static { + try { + Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + unsafe = (Unsafe) unsafeField.get(null); + + Field theInternalUnsafeField = forceAccessible(Unsafe.class.getDeclaredField("theInternalUnsafe"), true); + theInternalUnsafe = theInternalUnsafeField.get(null); + + Class theInternalUnsafeClass = theInternalUnsafe.getClass(); + objectFieldOffset0 = forceAccessible(theInternalUnsafeClass.getDeclaredMethod("objectFieldOffset0", Field.class), true); + staticFieldOffset0 = forceAccessible(theInternalUnsafeClass.getDeclaredMethod("staticFieldOffset0", Field.class), true); + } catch (IllegalAccessException | NoSuchFieldException | NoSuchMethodException e) { + throw new RuntimeException("Initialization of ReflectionUtils failed", e); + } + } + + static long getObjectFieldOffset(Field f) { + try { + return (long) objectFieldOffset0.invoke(theInternalUnsafe, f); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + static long getStaticFieldOffset(Field f) { + try { + return (long) staticFieldOffset0.invoke(theInternalUnsafe, f); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + /** + * takes an object, makes it accessible and returns it + * @param o object to be made accessible + * @param accessible should be accessible + * @return the same object that was passed in but accessible + */ + public static T forceAccessible(T o, boolean accessible) { + unsafe.putBoolean(o, 12, accessible); + return o; + } + + public static Boolean isAccessible(AccessibleObject o) { + return unsafe.getBoolean(o, 12); + } + + static String methodToString(Class clazz, String name, Class[] argTypes) { + return clazz.getName() + '.' + name + + ((argTypes == null || argTypes.length == 0) ? + "()" : + Arrays.stream(argTypes) + .map(c -> c == null ? "null" : c.getName()) + .collect(Collectors.joining(",", "(", ")"))); + } +} \ No newline at end of file diff --git a/src/test/java/ConstructorTest.java b/src/test/java/ConstructorTest.java new file mode 100644 index 0000000..12ec2ad --- /dev/null +++ b/src/test/java/ConstructorTest.java @@ -0,0 +1,37 @@ +import org.junit.jupiter.api.Test; +import vars.TestVar; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static reflection.ConstructorReflection.*; + + +public class ConstructorTest { + + @Test + public void noConstructor() { + TestVar testVar = createInstanceWithoutConstructor(TestVar.class); + assertEquals(0, testVar.usedContructor); + } + + @Test + public void getPrivateConstructorInt() { + TestVar testVar = null; + try { + testVar = useConstructor(getConstructor(TestVar.class, int.class), 1); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + assertEquals(1, testVar.usedContructor); + } + + @Test + public void getPrivateConstructor() { + TestVar testVar = null; + try { + testVar = useConstructor(getConstructor(TestVar.class)); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + assertEquals(1, testVar.usedContructor); + } +} diff --git a/src/test/java/FieldTest.java b/src/test/java/FieldTest.java new file mode 100644 index 0000000..c512cd8 --- /dev/null +++ b/src/test/java/FieldTest.java @@ -0,0 +1,56 @@ +import org.junit.jupiter.api.Test; +import reflection.FieldReflection; +import vars.TestVar; +import vars.TestVar2; + +import static org.junit.jupiter.api.Assertions.*; +import static reflection.FieldReflection.*; + +public class FieldTest { + + @Test + public void setPublicFinalInt() { + TestVar testVar = new TestVar(); + + try { + setFieldValue(getField(TestVar.class, "publicFinalInt"), testVar, 1); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + assertEquals(1, testVar.publicFinalInt); + } + + @Test + public void setPrivateStaticLong() { + try { + setStaticFieldValue(getField(TestVar.class, "privateStaticLong"), 1L); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + assertEquals(1, TestVar.getPrivateStaticLong()); + } + + @Test + public void setPrivateVolatileFloat() { + TestVar testVar = new TestVar(); + + try { + setFieldValue(getField(TestVar.class, "privateVolatileFloat"), testVar, 1F); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + assertEquals(1, testVar.getPrivateVolatileFloat()); + } + + @Test + public void setPublicFinalObjectSuperclass() { + TestVar2 testVar2 = new TestVar2(); + + try { + setFieldValue(getField(TestVar2.class, "publicFinalObject", true), testVar2, testVar2); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + assertEquals(testVar2, testVar2.publicFinalObject); + } +} diff --git a/src/test/java/MethodTest.java b/src/test/java/MethodTest.java new file mode 100644 index 0000000..3256cfd --- /dev/null +++ b/src/test/java/MethodTest.java @@ -0,0 +1,98 @@ +import org.junit.jupiter.api.Test; +import vars.TestVar; +import vars.TestVar2; + +import static org.junit.jupiter.api.Assertions.*; +import static reflection.FieldReflection.getFieldValue; +import static reflection.FieldReflection.getStaticFieldValue; +import static reflection.MethodReflection.*; + +public class MethodTest { + + @Test + public void getPrivateMethodWithout() { + TestVar testVar = new TestVar(); + try { + useMethod(getMethod(TestVar.class, "privateMethod"), testVar); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + assertTrue(testVar.wasMethodUsed); + } + + @Test + public void getPrivateMethodWithAndReturnValue() { + TestVar testVar = new TestVar(); + int i = 1; + try { + assertEquals(i, useMethod(getMethod(TestVar.class, "privateMethod", int.class), testVar, i)); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + assertTrue(testVar.wasMethodUsed); + } + + @Test + public void getPrivateStaticMethodWithout() { + try { + useStaticMethod(getMethod(TestVar.class, "privateStaticMethod")); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + + assertTrue(TestVar.wasStaticMethodUsed); + } + + @Test + public void getPrivateStaticMethodWith() { + int i = 1; + + try { + assertEquals(i, useStaticMethod(getMethod(TestVar.class, "privateStaticMethod", int.class), i)); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + assertTrue(TestVar.wasStaticMethodUsedWith); + } + + @Test + public void getPrivateMethodWithoutSuperclass() { + TestVar2 fakeClass2 = new TestVar2(); + try { + useMethod(getMethod(TestVar.class, "privateMethod"), fakeClass2); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + assertTrue(fakeClass2.wasMethodUsed); + } + + @Test + public void getPrivateMethodWithSuperclass() { + TestVar2 fakeClass2 = new TestVar2(); + try { + useMethod(getMethod(TestVar.class, "privateMethod", int.class), fakeClass2, 1); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + assertTrue(fakeClass2.wasMethodUsed); + } + + @Test + public void getPrivateStaticMethodWithoutSuperclass() { + try { + useStaticMethod(getMethod(TestVar2.class, "privateStaticMethodSuperclass", true)); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + assertTrue(TestVar.wasStaticMethodUsedSuperclass); + } + + @Test + public void getUnusable() { + try { + assertEquals(getStaticFieldValue(Class.forName("jdk.internal.org.objectweb.asm.Type").getField("BYTE_TYPE")), useMethod(Class.forName("jdk.internal.org.objectweb.asm.Type").getDeclaredMethod("getType", String.class), null, "B")); + } catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException e) { + e.printStackTrace(); + } + } +} diff --git a/src/test/java/UtilsTest.java b/src/test/java/UtilsTest.java new file mode 100644 index 0000000..879222b --- /dev/null +++ b/src/test/java/UtilsTest.java @@ -0,0 +1,23 @@ +import org.junit.jupiter.api.Test; +import java.lang.reflect.Field; +import static org.junit.jupiter.api.Assertions.*; +import static reflection.FieldReflection.getField; +import static reflection.ReflectionUtils.*; + +public class UtilsTest { + + @Test + public void forceAccessibleField() { + Field f = null; + try { + f = getField(Field.class, "clazz"); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + assertFalse(isAccessible(f)); + forceAccessible(f, true); + assertTrue(isAccessible(f)); + forceAccessible(f, false); + assertFalse(isAccessible(f)); + } +} diff --git a/src/test/java/vars/TestVar.java b/src/test/java/vars/TestVar.java new file mode 100644 index 0000000..def7e01 --- /dev/null +++ b/src/test/java/vars/TestVar.java @@ -0,0 +1,61 @@ +package vars; + +public class TestVar { + + public final int publicFinalInt; + public final Object publicFinalObject = new Object(); + private static long privateStaticLong = 0; + public final int usedContructor; + private volatile float privateVolatileFloat = 0; + + public boolean wasMethodUsed = false; + public static boolean wasStaticMethodUsed = false; + public static boolean wasStaticMethodUsedWith = false; + public static boolean wasStaticMethodUsedSuperclass = false; + + + private void privateMethod() { + wasMethodUsed = true; + } + + private int privateMethod(int i) { + wasMethodUsed = true; + return i; + } + + private static void privateStaticMethod() { + wasStaticMethodUsed = true; + } + + private static int privateStaticMethod(int i) { + wasStaticMethodUsedWith = true; + return i; + } + + private static void privateStaticMethodSuperclass() { + wasStaticMethodUsedSuperclass = true; + } + + public static long getPrivateStaticLong() { + return privateStaticLong; + } + + public float getPrivateVolatileFloat() { + return privateVolatileFloat; + } + + public TestVar() { + publicFinalInt = 0; + usedContructor = 1; + } + + public TestVar(int i) { + publicFinalInt = 0; + usedContructor = 1; + } + + TestVar(Object obj, char c) { + publicFinalInt = 0; + usedContructor = 1; + } +} diff --git a/src/test/java/vars/TestVar2.java b/src/test/java/vars/TestVar2.java new file mode 100644 index 0000000..2de40c7 --- /dev/null +++ b/src/test/java/vars/TestVar2.java @@ -0,0 +1,6 @@ +package vars; + +public class TestVar2 extends TestVar { + + +}