转载

Java中的反射

个人对反射的理解就是在运行时动态去获取、操作Java程序,反射赋予了Java这门静态语言动态执行的能力。

反射的对象是在JVM中运行时的方法、属性、构造函数。

在现代化Java框架中都不可避免的运用到了反射,赋予程序更好的动态执行能力。

过于随意的反射操作也会带来一些安全隐患,比如反序列化中知名的cc链、weblogic中的coherence链,以及tomcat中rmi工厂类的绕过payload,都是由于其过于动态的反射执行(不过也不全是反射的问题,它们也只是漏洞利用中的一环)。

由于其可以动态执行函数,且不受private等修饰符限制等特点,一些webshell绕过,执行恶意payload时也经常会用到反射。

如下就是一个反射调用 Runtime.exec 的例子,本文会尝试从Java源码的角度去学习下反射是如何执行的(native方法暂时略过)。

Method m = Runtime.class.getDeclaredMethod("exec", String.class);
m.invoke(Runtime.getRuntime(), "calc");

权限校验

Java中的反射

首先来到Method的invoke方法中,会首先校验Method的override值,这个值是从AccessibleObject中继承下来的。

Field、Method、Constructor三个类都继承了AccessibleObject。

Java中的反射

我们在对private方法进行invoke前会对其进行setAccessible操作,其实就是改变了这个override值。

随后通过 Reflection.quickCheckMemberAccess 进行一次快速的权限校验,实际就是判断其是否是public。

Java中的反射

如果不通过,则会进入进一步的权限检测,会先通过 Reflection.getCallerClass 获取其调用者,这是一个native方法,由于C/C++功底极烂的原因,这篇文章中涉及cpp源码的部分就不深入。

随后在进行一次快速的权限校验,主要是对函数修饰符和调用者caller进行一些判断。

Java中的反射

如果都不通过,则会进行一次慢检查。

Java中的反射

其核心在 Reflection.verifyMemberAccess 中,主要是对函数修饰符、所在包、子父类关系进行判断。

public static boolean verifyMemberAccess(Class<?> var0, Class<?> var1, Object var2, int var3) {
    boolean var4 = false;
    boolean var5 = false;
    if (var0 == var1) {
        return true;
    } else {
        if (!Modifier.isPublic(getClassAccessFlags(var1))) {
            var5 = isSameClassPackage(var0, var1);
            var4 = true;
            if (!var5) {
                return false;
            }
        }

        if (Modifier.isPublic(var3)) {
            return true;
        } else {
            boolean var6 = false;
            if (Modifier.isProtected(var3) && isSubclassOf(var0, var1)) {
                var6 = true;
            }

            if (!var6 && !Modifier.isPrivate(var3)) {
                if (!var4) {
                    var5 = isSameClassPackage(var0, var1);
                    var4 = true;
                }

                if (var5) {
                    var6 = true;
                }
            }

            if (!var6) {
                return false;
            } else {
                if (Modifier.isProtected(var3)) {
                    Class var7 = var2 == null ? var1 : var2.getClass();
                    if (var7 != var0) {
                        if (!var4) {
                            var5 = isSameClassPackage(var0, var1);
                            var4 = true;
                        }

                        if (!var5 && !isSubclassOf(var7, var0)) {
                            return false;
                        }
                    }
                }

                return true;
            }
        }
    }
}

MethodAccessor

权限校验通过之后,回到 Method.invoke 中,会去获取对应 MethodAccessor ,再调用 MethodAccessorinvoke 方法

Java中的反射

所以,实际上 Methodinvoke 反射,是委托给 MethodAccessor 来进行处理的。

MethodAccessor的实现

MethodAccessor 实际上是一个接口,其拥有两个具体实现( MethodAccessorImpl 只是一个抽象实现)

Java中的反射
DelegatingMethodAccessorImpl
NativeMethodAccessorImpl

然而实际上还有另外一个 GeneratedMethodAccessor ,它是由Java动态生成的 MethodAccessor ,它们之间的对应关系如下。

Java中的反射

所以实际上真正执行反射方法的应该是 NativeMethodAccessorGeneratedMethodAccessor ,它们直接有一些差异,我直接引用下大佬们的。

就像注释里说的,实际的MethodAccessor实现有两个版本,一个是Java版本,一个是native版本,两者各有特点。初次启动时Method.invoke()和Constructor.newInstance()方法采用native方法要比Java方法快3-4倍,而启动后native方法又要消耗额外的性能而慢于Java方法。也就是说,Java实现的版本在初始化时需要较多时间,但长久来说性能较好;native版本正好相反,启动时相对较快,但运行时间长了之后速度就比不过Java版了。这是HotSpot的优化方式带来的性能特性,同时也是许多虚拟机的共同点:跨越native边界会对优化有阻碍作用,它就像个黑箱一样让虚拟机难以分析也将其内联,于是运行时间长了之后反而是托管版本的代码更快些。

简单来说就是最开始时 NativeMethodAccessor 的性能较快,而后续由于JVM虚拟机优化的原因, GeneratedMethodAccessor 的性能会更加,因此Java中会设置一定策略,使其最开始是用 NativeMethodAccessor 进行反射,当同一个方法的反射执行次数到达一定阈值时,转为 GeneratedMethodAccessor 进行反射。

Method的root节点

acquireMethodAccessor 获取 MethodAccessor 的代码中,会先尝试从 root 中读取对应的 MethodAccessor ,如果为null,则通过 reflectionFactory 生成一个对应的 MethodAccessor

Java中的反射

先来看下这个root是什么呢?

通过debug可以看到,实际上 root 也是一个和当前Method相同类型的Method,但指向的并不是同一个实例。

Java中的反射

实际上每个Java方法都有一个唯一的 Method 对象作为 root 对象。我们获取到的 Method 实际上只是这个 root 的一个副本。这样就可以将 MethodAccessor 作用在这个 root 上,并被我们获取到的每一个副本所复用。

第一次调用反射时 rootmethodAccessor 属性为空,需要通过 reflectionFactory.newMethodAccessor 去获取对应实例。

默认情况下, newMethodAccessor 中会生成 DelegatingMethodAccessorImplNativeMethodAccessorImpl ,并对两者进行了相互之间的关系绑定(DelegatingMethodAccessorImpl是由 NativeMethodAccessorImpl 实例化来的,并且将 NativeMethodAccessorImplparent 属性设置为了 DelegatingMethodAccessorImpl

NativeMethodAccessorImpl var2 = new NativeMethodAccessorImpl(var1);
DelegatingMethodAccessorImpl var3 = new DelegatingMethodAccessorImpl(var2);
var2.setParent(var3);
return var3;

前面也提到过, DelegatingMethodAccessorImpl 其实只是一个中间代理层,所以这里也就意味着其会调用 NativeMethodAccessorImpl 进行方法反射。

到此,获取到了进行反射的 MethodAccessor ,并将其设置为 Methodroot 节点的属性。

MethodAccessor.invoke

可以看到 DelegatingMethodAccessorImpl 将反射委派给了之前生成的 NativeMethodAccessorImpl

Java中的反射

NativeMethodAccessorImpl 中会设置一个计数器,每次调用时计数+1,当超过一定阈值时,就会通过 MethodAccessorGenerator 生成 GeneratedMethodAccessor ,此后每次 DelegatingMethodAccessorImpl 的invoke则会委派给生成的 GeneratedMethodAccessor

这个阈值是 ReflectionFactory 中的一个常量,默认为15。

public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
    if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
        MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
        this.parent.setDelegate(var3);
    }

    return invoke0(this.method, var1, var2);
}

至于之后的invoke0,则是native的部分,这里暂不展开了。

MethodAccessorGenerator

MethodAccessorGenerator 又是如何生成 GeneratedMethodAccessor 的呢。

可以跟到 MethodAccessorGeneratorgenerate 方法中

private MagicAccessorImpl generate(final Class<?> var1, String var2, Class<?>[] var3, Class<?> var4, Class<?>[] var5, int var6, boolean var7, boolean var8, Class<?> var9) {
    ByteVector var10 = ByteVectorFactory.create();
    this.asm = new ClassFileAssembler(var10);
    this.declaringClass = var1;
    this.parameterTypes = var3;
    this.returnType = var4;
    this.modifiers = var6;
    this.isConstructor = var7;
    this.forSerialization = var8;
    this.asm.emitMagicAndVersion();
    short var11 = 42;
    boolean var12 = this.usesPrimitiveTypes();
    if (var12) {
        var11 = (short)(var11 + 72);
    }

    if (var8) {
        var11 = (short)(var11 + 2);
    }

    var11 += (short)(2 * this.numNonPrimitiveParameterTypes());
    this.asm.emitShort(add(var11, (short)1));
    final String var13 = generateName(var7, var8);
    this.asm.emitConstantPoolUTF8(var13);
    this.asm.emitConstantPoolClass(this.asm.cpi());
    this.thisClass = this.asm.cpi();
    if (var7) {
        if (var8) {
            this.asm.emitConstantPoolUTF8("sun/reflect/SerializationConstructorAccessorImpl");
        } else {
            this.asm.emitConstantPoolUTF8("sun/reflect/ConstructorAccessorImpl");
        }
    } else {
        this.asm.emitConstantPoolUTF8("sun/reflect/MethodAccessorImpl");
    }

    this.asm.emitConstantPoolClass(this.asm.cpi());
    this.superClass = this.asm.cpi();
    this.asm.emitConstantPoolUTF8(getClassName(var1, false));
    this.asm.emitConstantPoolClass(this.asm.cpi());
    this.targetClass = this.asm.cpi();
    short var14 = 0;
    if (var8) {
        this.asm.emitConstantPoolUTF8(getClassName(var9, false));
        this.asm.emitConstantPoolClass(this.asm.cpi());
        var14 = this.asm.cpi();
    }

    this.asm.emitConstantPoolUTF8(var2);
    this.asm.emitConstantPoolUTF8(this.buildInternalSignature());
    this.asm.emitConstantPoolNameAndType(sub(this.asm.cpi(), (short)1), this.asm.cpi());
    if (this.isInterface()) {
        this.asm.emitConstantPoolInterfaceMethodref(this.targetClass, this.asm.cpi());
    } else if (var8) {
        this.asm.emitConstantPoolMethodref(var14, this.asm.cpi());
    } else {
        this.asm.emitConstantPoolMethodref(this.targetClass, this.asm.cpi());
    }

    this.targetMethodRef = this.asm.cpi();
    if (var7) {
        this.asm.emitConstantPoolUTF8("newInstance");
    } else {
        this.asm.emitConstantPoolUTF8("invoke");
    }

    this.invokeIdx = this.asm.cpi();
    if (var7) {
        this.asm.emitConstantPoolUTF8("([Ljava/lang/Object;)Ljava/lang/Object;");
    } else {
        this.asm.emitConstantPoolUTF8("(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");
    }

    this.invokeDescriptorIdx = this.asm.cpi();
    this.nonPrimitiveParametersBaseIdx = add(this.asm.cpi(), (short)2);

    for(int var15 = 0; var15 < var3.length; ++var15) {
        Class var16 = var3[var15];
        if (!isPrimitive(var16)) {
            this.asm.emitConstantPoolUTF8(getClassName(var16, false));
            this.asm.emitConstantPoolClass(this.asm.cpi());
        }
    }

    this.emitCommonConstantPoolEntries();
    if (var12) {
        this.emitBoxingContantPoolEntries();
    }

    if (this.asm.cpi() != var11) {
        throw new InternalError("Adjust this code (cpi = " + this.asm.cpi() + ", numCPEntries = " + var11 + ")");
    } else {
        this.asm.emitShort((short)1);
        this.asm.emitShort(this.thisClass);
        this.asm.emitShort(this.superClass);
        this.asm.emitShort((short)0);
        this.asm.emitShort((short)0);
        this.asm.emitShort((short)2);
        this.emitConstructor();
        this.emitInvoke();
        this.asm.emitShort((short)0);
        var10.trim();
        final byte[] var17 = var10.getData();
        return (MagicAccessorImpl)AccessController.doPrivileged(new PrivilegedAction<MagicAccessorImpl>() {
            public MagicAccessorImpl run() {
                try {
                    return (MagicAccessorImpl)ClassDefiner.defineClass(var13, var17, 0, var17.length, var1.getClassLoader()).newInstance();
                } catch (IllegalAccessException | InstantiationException var2) {
                    throw new InternalError(var2);
                }
            }
        });
    }
}

大致可以知道是通过asm的技术,动态生成字节码,技术细节就先暂不深究。

暂时以 Runtime.exec 的反射为例,看下动态生成的字节码大体长啥样。

public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
    public GeneratedMethodAccessor1() {
    }

    public Object invoke(Object var1, Object[] var2) throws InvocationTargetException {
        if (var1 == null) {
            throw new NullPointerException();
        } else {
            Runtime var10000;
            String var10001;
            try {
                var10000 = (Runtime)var1;
                if (var2.length != 1) {
                    throw new IllegalArgumentException();
                }

                var10001 = (String)var2[0];
            } catch (NullPointerException | ClassCastException var4) {
                throw new IllegalArgumentException(var4.toString());
            }

            try {
                return var10000.exec(var10001);
            } catch (Throwable var3) {
                throw new InvocationTargetException(var3);
            }
        }
    }
}

可以看到,是直接将反射的调用转换成了Java代码层面的 Runtime.exec 执行。

总结

最后,借用一下参考中的图,反射的执行流程大致如下。

1、权限检查

2、通过反射工厂生成对应的MethodAccessor,并赋值给root节点

3、少量反射时直接通过native方法进行反射调用

4、当反射超过一定阈值时(15次),则会通过asm的方式动态生成字节码,后续的invoke调用都会直接转换java method的方式进行调用。

Java中的反射
原文  https://www.kingkk.com/2020/07/Java中的反射/
正文到此结束
Loading...