Java中的反射

Posted by kingkk on 2020-07-19

前言

个人对反射的理解就是在运行时动态去获取、操作Java程序,反射赋予了Java这门静态语言动态执行的能力。
反射的对象是在JVM中运行时的方法、属性、构造函数。
在现代化Java框架中都不可避免的运用到了反射,赋予程序更好的动态执行能力。
过于随意的反射操作也会带来一些安全隐患,比如反序列化中知名的cc链、weblogic中的coherence链,以及tomcat中rmi工厂类的绕过payload,都是由于其过于动态的反射执行(不过也不全是反射的问题,它们也只是漏洞利用中的一环)。
由于其可以动态执行函数,且不受private等修饰符限制等特点,一些webshell绕过,执行恶意payload时也经常会用到反射。
如下就是一个反射调用Runtime.exec的例子,本文会尝试从Java源码的角度去学习下反射是如何执行的(native方法暂时略过)。

1
2
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中,主要是对函数修饰符、所在包、子父类关系进行判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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方法

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

MethodAccessor的实现

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

  • DelegatingMethodAccessorImpl
  • NativeMethodAccessorImpl

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

所以实际上真正执行反射方法的应该是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

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

实际上每个Java方法都有一个唯一的Method对象作为root对象。我们获取到的Method实际上只是这个root的一个副本。这样就可以将MethodAccessor作用在这个root上,并被我们获取到的每一个副本所复用。
第一次调用反射时rootmethodAccessor属性为空,需要通过reflectionFactory.newMethodAccessor去获取对应实例。

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

1
2
3
4
NativeMethodAccessorImpl var2 = new NativeMethodAccessorImpl(var1);
DelegatingMethodAccessorImpl var3 = new DelegatingMethodAccessorImpl(var2);
var2.setParent(var3);
return var3;

前面也提到过,DelegatingMethodAccessorImpl其实只是一个中间代理层,所以这里也就意味着其会调用NativeMethodAccessorImpl进行方法反射。
到此,获取到了进行反射的MethodAccessor,并将其设置为Methodroot节点的属性。

MethodAccessor.invoke

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

NativeMethodAccessorImpl中会设置一个计数器,每次调用时计数+1,当超过一定阈值时,就会通过MethodAccessorGenerator生成GeneratedMethodAccessor,此后每次DelegatingMethodAccessorImpl的invoke则会委派给生成的GeneratedMethodAccessor
这个阈值是ReflectionFactory中的一个常量,默认为15。

1
2
3
4
5
6
7
8
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方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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的反射为例,看下动态生成的字节码大体长啥样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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的方式进行调用。

References

https://www.sczyh30.com/posts/Java/java-reflection-2/
http://rui0.cn/archives/1112