分析下原生的Java序列化与反序列化流程,为了尽量覆盖各种情况,需要找一个比较复杂的对象进行调试,这里选择CC1链。没有覆盖到的情况结合文档分析。
先看下CC1的payload,环境是jdk8u65
1 | public class CC1Test |
类结构是嵌套的,由外到内是大致是AnnotationInvocationHandler->Proxy->LazyMap->ChainedTransformer->Transformer[]
序列化流程
先看ObjectOutputStream的构造函数
1 | public ObjectOutputStream(OutputStream out) throws IOException { |
verifySubclass是当前类是ObjectOutputStream子类是做的校验,基本会忽略。
bout是BlockDataOutputStream输出流,用来真正存放数据的。
handles是一个哈希表,映射对象的引用,用来节约重复的对象的空间。
subs是一个哈希表,映射被替换的对象。
enableOverride默认是false,如果是true则调用writeObjectOverride代替writeObject进行序列化。只有调用ObjectOutputStream无参构造方法时会赋值为true,代表跟原本对象无关了。很少见。
writeStreamHeader写入数据头
1 | protected void writeStreamHeader() throws IOException { |
其实就是传说中的序列化标志aced0005
然后设置data blcok格式,这是为了兼容性,不重要。
然后跟进ObjectOutputStream#writeObject
1 | public final void writeObject(Object obj) throws IOException { |
如果配置了enableOverride,就调用writeObjectOverride,否则调用writeObject0。基本都是后一种情况,跟进writeObject0,比较长拆开看
1 | private void writeObject0(Object obj, boolean unshared) |
如注释里所写,先处理之前写入过的对象(第二个case)和不能替换的对象(第一个case)。然后判断写入对象的类型,如果是class调用writeClass,如果是ObjectStreamClass调用writeClassDesc写入。
当前subs和handles都是空的,obj是一个AnnotationInvocationHandler,所以不会进入这几个case。
1 | // check for replacement object |
然后处理替换的对象,首先会获取一个ObjectStreamClass,这就是类的描述符,里面记录了这个类和序列化有关的属性,比如是不是动态代理,能不能序列化,能不能外部化,是不是重写了writeObject、readObject等等。如果这个类定义了一个叫writeReplace的方法,就会调用重写的writeReplace将obj和cl替换为对应返回值。
然后如果开启了enableReplace,会调用replaceObject,并将obj和cl替换为对应的返回值。
这两个替换其实很像,不太清楚为什么要有两个。硬说区别的话,writeReplace只要定义了就会调用,而replaceObject需要调用enableReplaceObject开启enableReplace才可以。总之都很少用到。
最后,如果对象被替换过了,重新做一遍前面的判断。
1 | // remaining cases |
处理完替换对象相关的判断,来到真正的序列化部分,这里判断obj的类型,对于String、Array、Enum这几种类型有特殊的序列化方法。除此之外都会进入writeOrdinaryObject中。当然如果没有继承Serializable会抛NotSerializableException异常
1 | private void writeOrdinaryObject(Object obj, |
这里是写入一般对象的逻辑:
1、首先写一个字节标识0x73代表该对象是Object
2、然后调用writeClassDesc写入类描述符
3、之后调用handles.assign也就是代表这个对象已经序列化过,后续再序列化的话只需要保存引用就可以。
4、之后写入对象具体数据,根据类是否可以外部化调用writeExternalData或writeSerialData,实际上就是调用writeExternal或writeObject。
先看writeClassDesc
1 | private void writeClassDesc(ObjectStreamClass desc, boolean unshared) |
如果类描述符是null就写入null,如果之前写入过这个类描述符就writeHandle写引用来代替。如果是动态代理就调用writeProxyDesc,不是就调用writeNonProxyDesc。那么当前实际会进入writeNonProxyDesc,因为当前的AnnotationInvocationHandler并不是动态代理。
1 | private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared) |
首先写一个字节TC_CLASSDESC(0x72)代表写入的是类描述符,然后向handles里保存当前类描述符,然后根据序列化协议调用ObjectStreamClass#writeNonProxy或writeClassDescriptor,目前协议版本都是2了,调用writeClassDescriptor
1 | protected void writeClassDescriptor(ObjectStreamClass desc) |
实际上是一样的。都是ObjectStreamClass#writeNonProxy
1 | void writeNonProxy(ObjectOutputStream out) throws IOException { |
写入类名,写入SerialVersionUID,写入标志位,写入字段数量,然后和循环写入字段的类型和名字。比如当前类有两个字段,第一个是memberValues,分别写入L、memberValues、Ljava/util/Map; 第二个是type,写入L、type、Ljava/lang/Class;
之后返回writeNonProxyDesc,调用annotateClass,默认是空函数,可以重写这个方法,在写入类描述符之后再写一些信息。如果这么做的话,对应的在反序列化时需要重写resolveClass来处理。一般不会调用,然后写TC_ENDBLOCKDATA标识符,然后调用又writeClassDesc写入父类的标识符,递归的感觉。当前desc就是null了,因为父类Object不能序列化。
回到writeOrdinaryObject调用writeSerialData
1 | private void writeSerialData(Object obj, ObjectStreamClass desc) |
其实就是重写writeObject的话就调writeObject,不然就调defaultWriteFields
1 | private void defaultWriteFields(Object obj, ObjectStreamClass desc) |
首先获取类里所有prim的字段,也就是基础属性,包括boolean、byte、char、short、int、float、long、double这些,然后直接写入流。
之后获取非基础字段,循环调用writeObject0。
所以实际上java原生序列化是从外到内递归调用writeObject的,一直写入到所有字段都是基础属性为止。
那么继续跟进,下一层的两个对象分别是一个动态代理和一个Class,那么接下来分析动态代理对象的序列化过程:
对动态代理对象的序列化
前面的部分都一样,在writeClassDesc时会进入到writeProxyDesc分支:
1 | private void writeProxyDesc(ObjectStreamClass desc, boolean unshared) |
这里循环写入的是接口的名字,也就是java.util.Map。然后调用annotateProxyClass,一般也是空方法,之后一样调用writeClassDesc(desc.getSuperDesc(), false),这里父类描述符是Proxy类的描述符,会调用writeNonProxyDesc,因为它自己并不是代理类。和最外层一样这里就不跟了。
然后又到writeSerialData,这个动态代理类只有一个字段,值是一个AnnotationIvocationHandler,那么下一层就是序列化这个对象。
虽然这个AnnotationIvocationHandler和最外层的不是一个对象,但它们是同一个类,因此为了节省空间会用引用代替。
在writeClassDesc时会调用writeHandle
1 | private void writeHandle(int handle) throws IOException { |
用一个int来代替原来的好几个字符串。
然后继续下一层序列化,这时写入的是LazyMap和Override.Class。序列化LazyMap时前面都差不多,但在writeSerialData时会触发自身的writeObject
1 | private void writeObject(ObjectOutputStream out) throws IOException { |
先调用了空的defaultWriteObject又调用了正常的writeObject(map)
1 | public void defaultWriteObject() throws IOException { |
实际上还是原来的defaultWriteFields(obj, desc),然后重新调用了一次ObjectOutputStream#writeObject。
下一个对象是ChainedTransformer,没什么特别的,略过。
对数组的序列化
然后是Transformer数组,此时获取到desc是[Lorg.apache.commons.collections.Transformer;
在writeObject0中进入到writeArray
1 | private void writeArray(Object array, |
如果是prim类型就直接写,不是的话继续对数组里每个成员writeObject0。下一个就是ConstantTransformer,之后是其中的Runtime.class
对Class的序列化
对Class直接调用writeClass然后return,因为其中没有需要序列化传递的属性值。
1 | private void writeClass(Class<?> cl, boolean unshared) throws IOException { |
对String的序列化
序列化InvokerTransformer最后会获取到字符串内容,也就是getRuntime,此时调用writeString对字符串序列化
1 | private void writeString(String str, boolean unshared) throws IOException { |
实际上序列化过程都是大同小异的,基本都是写标识符、写类描述、字段名、内容。
反序列化流程
构造函数是类似的
1 | public ObjectInputStream(InputStream in) throws IOException { |
看readObject,先看前半部分
1 | public final Object readObject() |
同样判断override,然后调用readObject0,还有些对于嵌套反序列化的处理,不是很重要。
重点看readObject0
1 | private Object readObject0(boolean unshared) throws IOException { |
其实就是根据读取的标识符调用对应的方法,当前是TC_OBJECT,调用checkResolve(readOrdinaryObject(unshared))
看readOrdinaryObject,内容比较多,分开看
1 | private Object readOrdinaryObject(boolean unshared) |
首先读取类描述符,判断类型
1 | private ObjectStreamClass readClassDesc(boolean unshared) |
当前调用readNonProxyDesc
1 | private ObjectStreamClass readNonProxyDesc(boolean unshared) |
readClassDescriptor和序列化时候是对应的,就是读取字段,不细说了。之后注意会调用cl = resolveClass(readDesc)来获取反序列化对象的Class
1 | protected Class<?> resolveClass(ObjectStreamClass desc) |
默认是通过Class.forName直接加载的,但有些时候会重写resolveClass,比如shiro。resolveClass和序列化时的annotateClass是对应的,也可以在里面写一些关于类加载的逻辑,比如反序列化黑名单。
然后调用skipCustomData,一般就是读取一个结束标志位。也有可能在里面嵌套反序列化。
然后是desc.initNonProxy,功能是初始化类描述符,判断是否重写了readObject之类的。
回到readOrdinaryObject里
1 | Object obj; |
这里通过desc.newInstance()获取了一个obj
1 | /** |
注释写的是:如果是externalizable的类,就调用自己的public无参构造方法;如果是serializable的类,调用第一个不能序列化的父类的无参构造方法。那对于当前的AnnotationInvocationHandler来说就是Object类。Object是没有显示定义构造方法的,Java里没有定义的话默认就是有一个无参构造。
所以很容易会认为调用的是Object的无参构造方法,但这样就很奇怪,调用Object的构造方法怎么可能生成一个AnnotationInvocationHandler对象呢?
跟了一下,发现是jdk专门为反序列化做了些特殊处理,在ObjectStreamClass的构造函数里
1 | private ObjectStreamClass(final Class<?> cl) { |
cons赋值是这么一句cons = getSerializableConstructor(cl)
1 | private static Constructor<?> getSerializableConstructor(Class<?> cl) { |
调用了reflFactory.newConstructorForSerialization。这里用到了ReflectionFactory这个类,这是非常底层的一个类,用来实现某些特殊的反射功能,比如不调用构造方法创建对象。很多功能都是通过修改asm字节码的方式实现的,不细看了。newConstructorForSerialization这个方法的作用可以理解为利用一个构造方法来创建一个新的构造方法,这里相当于基于Object的构造方法生成了一个AnnotationInvocationHandler的构造方法,调用cons.newInstance时调用了Object的构造方法,但返回的是一个AnnotationInvocationHandler对象。
总之就是这样实现的,可以自己写个demo试下。尽量理解一下这样做的意义,先创建一个对应类的空对象,然后再赋值,大概是这样。
接着回到readOrdinaryObject
1 | if (desc.isExternalizable()) { |
接下来读取数据,还是根据externalizable和serializable区分,这里调用readSerialData
1 | private void readSerialData(Object obj, ObjectStreamClass desc) |
和序列化的时候差不多,判断是否重写了readObject,有就调用,没有就调用默认的defaultReadFields。这里还有个case会调用readObjectNoData,很少见,一般是在序列化和反序列化的类版本不同时调用,不细分析了。
进入AnnotationInvocationHandler#readObject,先调用ObjectInputStream#defaultReadObject,然后还是会调用defaultReadFields
1 | private void defaultReadFields(Object obj, ObjectStreamClass desc) |
和序列化类似,读基础类型,然后递归调用readObject0读复杂类型。所以反序列化可以理解为是从里往外的。
实际上后续的都比较类似了,就是根据读到的标识符来调用对应的方法读取数据。不同的类有不同的处理方式,比如读取动态代理类的时候会调用resolveProxyClass而不是resolveClass
1 | protected Class<?> resolveProxyClass(String[] interfaces) |
获取到的是一个动态代理类$Proxy0
读完数据后readOrdinaryObject会判断是否定义了readResolve
1 | if (obj != null && |
和writeReplace很像。
调用完readOrdinaryObject后会调用checkResolve
1 | private Object checkResolve(Object obj) throws IOException { |
和replaceObject功能很类似,如果开启了enableResolve,就会调用resolveObject的结果替换反序列化读到的对象,也就是把前面获取的返回值扔掉换成这个函数的返回值。
最后回到readObject的后半部分,执行完readObject0以后
1 | ClassNotFoundException ex = handles.lookupException(passHandle); |
调用了vlist.doCallbacks
1 | void doCallbacks() throws InvalidObjectException { |
调用了validateObject,如果list.obj重写了这个方法会调用,但一般不会调用。
总结
实际上序列化和反序列化的思路都是比较简单的,就是定义一种协议,通过标识符和数据来储存对象。读取的时候也是一样根据标识符来读取,一直到基础类型为止。
序列化和反序列化的默认过程都是从内到外的,但重写writeObject/readObject可能导致流程变化。
JDK提供了一些方法允许开发者改变序列化和反序列化的流程,序列化的包括writeReplace、replaceObject、annotateClass,反序列化的包括readResolve、resolveObject、resolveClass、readObjectNoData,分析时可以重点关注这些函数。
分析完了发现这个过程里没有Externalizable的,早知道分析rmi那个动态代理好了(
jdk8u71修复
顺便分析下jdk8u71对于cc1的修复,8u71中AnnotationInvocationHandler的readObject改成了这样
1 | private void readObject(java.io.ObjectInputStream s) |
也就是用s.readFields代替s.defaultReadObject()
1 | public ObjectInputStream.GetField readFields() |
调用了GetFieldImpl#readFields
1 | void readFields() throws IOException { |
实际上没什么区别还是会从里到外递归调用readObject0,那么先执行完的是内层的那个AnnotationInvocationHandler,也就是动态代理里面的那个handler对象,看下流程:
1 | Map<String, Object> mv = new LinkedHashMap<>(); |
此时memberValues是lazyMap,但实际上获取到lazyMap对象后,这里相当于把这个lazyMap拆开了,只留下了key和value,放到了新的LinkedHashMap里,然后赋值给mamberValues。那么实际上反序列化读出来的这个AnnotationInvocationHandler里面已经没有cc的payload了。
而外层的AnnotationInvocationHandler再调用streamVals.entrySet()时,虽然这个streamVals还是一个动态代理,但里面的AnnotationInvocationHandler里面已经没有lazyMap了,只有一个LinkedHashMap,那再对它调get也就执行不了代码了。
所以主要的修复逻辑就是new的这个LinkedHashMap,和getField没啥关系。
参考链接
https://docs.oracle.com/javase/8/docs/platform/serialization/spec/input.html
https://docs.oracle.com/javase/8/docs/platform/serialization/spec/output.html
https://www.bookstack.cn/read/anbai-inc-javaweb-sec/javase-JavaDeserialization-Serialization.md
https://www.cnblogs.com/binarylei/p/10989372.html