个人对反射的理解就是在运行时动态去获取、操作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");
首先来到Method的invoke方法中,会首先校验Method的override值,这个值是从AccessibleObject中继承下来的。
Field、Method、Constructor三个类都继承了AccessibleObject。
我们在对private方法进行invoke前会对其进行setAccessible操作,其实就是改变了这个override值。
随后通过 Reflection.quickCheckMemberAccess
进行一次快速的权限校验,实际就是判断其是否是public。
如果不通过,则会进入进一步的权限检测,会先通过 Reflection.getCallerClass
获取其调用者,这是一个native方法,由于C/C++功底极烂的原因,这篇文章中涉及cpp源码的部分就不深入。
随后在进行一次快速的权限校验,主要是对函数修饰符和调用者caller进行一些判断。
如果都不通过,则会进行一次慢检查。
其核心在 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; } } } }
权限校验通过之后,回到 Method.invoke
中,会去获取对应 MethodAccessor
,再调用 MethodAccessor
的 invoke
方法
所以,实际上 Method
的 invoke
反射,是委托给 MethodAccessor
来进行处理的。
MethodAccessor
实际上是一个接口,其拥有两个具体实现( MethodAccessorImpl
只是一个抽象实现)
DelegatingMethodAccessorImpl NativeMethodAccessorImpl
然而实际上还有另外一个 GeneratedMethodAccessor
,它是由Java动态生成的 MethodAccessor
,它们之间的对应关系如下。
所以实际上真正执行反射方法的应该是 NativeMethodAccessor
和 GeneratedMethodAccessor
,它们直接有一些差异,我直接引用下大佬们的。
就像注释里说的,实际的MethodAccessor实现有两个版本,一个是Java版本,一个是native版本,两者各有特点。初次启动时Method.invoke()和Constructor.newInstance()方法采用native方法要比Java方法快3-4倍,而启动后native方法又要消耗额外的性能而慢于Java方法。也就是说,Java实现的版本在初始化时需要较多时间,但长久来说性能较好;native版本正好相反,启动时相对较快,但运行时间长了之后速度就比不过Java版了。这是HotSpot的优化方式带来的性能特性,同时也是许多虚拟机的共同点:跨越native边界会对优化有阻碍作用,它就像个黑箱一样让虚拟机难以分析也将其内联,于是运行时间长了之后反而是托管版本的代码更快些。
简单来说就是最开始时 NativeMethodAccessor
的性能较快,而后续由于JVM虚拟机优化的原因, GeneratedMethodAccessor
的性能会更加,因此Java中会设置一定策略,使其最开始是用 NativeMethodAccessor
进行反射,当同一个方法的反射执行次数到达一定阈值时,转为 GeneratedMethodAccessor
进行反射。
在 acquireMethodAccessor
获取 MethodAccessor
的代码中,会先尝试从 root
中读取对应的 MethodAccessor
,如果为null,则通过 reflectionFactory
生成一个对应的 MethodAccessor
。
先来看下这个root是什么呢?
通过debug可以看到,实际上 root
也是一个和当前Method相同类型的Method,但指向的并不是同一个实例。
实际上每个Java方法都有一个唯一的 Method
对象作为 root
对象。我们获取到的 Method
实际上只是这个 root
的一个副本。这样就可以将 MethodAccessor
作用在这个 root
上,并被我们获取到的每一个副本所复用。
第一次调用反射时 root
的 methodAccessor
属性为空,需要通过 reflectionFactory.newMethodAccessor
去获取对应实例。
默认情况下, newMethodAccessor
中会生成 DelegatingMethodAccessorImpl
和 NativeMethodAccessorImpl
,并对两者进行了相互之间的关系绑定(DelegatingMethodAccessorImpl是由 NativeMethodAccessorImpl
实例化来的,并且将 NativeMethodAccessorImpl
的 parent
属性设置为了 DelegatingMethodAccessorImpl
)
NativeMethodAccessorImpl var2 = new NativeMethodAccessorImpl(var1); DelegatingMethodAccessorImpl var3 = new DelegatingMethodAccessorImpl(var2); var2.setParent(var3); return var3;
前面也提到过, DelegatingMethodAccessorImpl
其实只是一个中间代理层,所以这里也就意味着其会调用 NativeMethodAccessorImpl
进行方法反射。
到此,获取到了进行反射的 MethodAccessor
,并将其设置为 Method
的 root
节点的属性。
可以看到 DelegatingMethodAccessorImpl
将反射委派给了之前生成的 NativeMethodAccessorImpl
。
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
又是如何生成 GeneratedMethodAccessor
的呢。
可以跟到 MethodAccessorGenerator
的 generate
方法中
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的方式进行调用。