0x00 前言
写这篇文章的初衷,是我做JavaDeserializeLabs的Lab6,需要分析原生反序列化的流程,便想着从序列化开始分析。
0x01 demo
先创建一个Person类,作为序列化的对象:
1 | import java.io.Serializable; |
主类Main:
1 | import java.io.ByteArrayInputStream; |
0x02 创建对象输出流
断点打在如下位置:
跟进到ObjectOutputStream的有参构造方法:
首先就是设置对象输出流的协议版本。
这里扩展一下Data Block模式:
在JDK 1.2中,有必要修改和JDK 1.1不兼容的字节流格式;为了处理这种情况,向前兼容性是必须的,一个兼容标记将会写入到字节流中,这个兼容标记是类似PROTOCOL_VERSION的格式,ObjectOutputStream中的useProtocolVersion方法会接收一个参数以表示写入的可序列化字节流的协议版本。
使用的字节流协议版本如下:
- ObjectStreamConstants.PROTOCOL_VERSION_1:表示最初序列化字节流的格式;
- ObjectStreamConstants.PROTOCOL_VERSION_2:表示新的外部字节流格式,基础类型的数据将会使用数据块【Data-Block】的模式写入字节流,它以标记TC_ENDBLOCKDATA结束
数据块的边界是标准化的,使用数据块模式写入字节流的基础类型的数据通常不能超过1024字节长度,这种变化的好处是固定以及规范化序列化数据格式,有利于其向前和向后的兼容性。
JDK1.2默认使用PROTOCOL_VERSION_2JDK1.1默认使用PROTOCOL_VERSION_1JDK 1.1.7版本以及以上的版本可读取以上的两种版本,而JDK 1.1.7之前的版本只能读取PROTOCOL_VERSION_1版本;
然后调用verifySubclass方法处理缓存信息,验证过程确保该类或子类的实例可以安全地被构造:
往下是几个属性的赋值:
属性名 | 类型 | 作用 |
---|---|---|
bout | ObjectInputStream.BlockDataOutputStream | 类似“容器”,程序将序列化的字节内容一步步存放到bout里 |
handles | HandleTable | 一个哈希表,表示从对象到引用的映射 |
subs | HandleTable | 一个哈希表,表示从对象到“替换对象”的一个映射关系 |
enableOverride | boolean | 决定在序列化Java对象时选用writeObjectOverride方法还是writeObjec方法 |
赋值以后,调用了writeStreamHeader方法,向bout中写入魔术头以及版本号:
往下是一个分支,extendedDebugInfo属性在Java序列化中用于提供更详细的调试信息,非本文重点,略。
成功初始化ObjectOutputStream,进入writeObject方法。
0x03 writeObject方法
跟进writeObject方法:
enableOverride=false,调用writeObject0方法:
整个writeObject0方法的代码比较长,就不贴图了,直接从第一行开始。
首先代码先关闭输出流的Data Block模式,并且将原始模式赋值给变量oldMode
1 | boolean oldMode = bout.setBlockDataMode(false); |
往下走,依次判断obj(Person类实例)是否存在替换对象和引用,以及是否属于一些特定类:
当然都不符合。接着走,根据obj的类型创建了desc对象:
后面经常用到desc对象:
跳过两个if分支,关注下一个if分支。因为传入对象实现了Serializable接口,调用writeOrdinaryObject法将数据写入字节流:
先调用desc.checkSerialize(),该方法检查当前对象是否是一个可序列化对象,如果不是便会终止本次序列化并抛出newInvalidClassException()错误:
接着走,如果传入对象是一个可序列化对象,bout写入TC_OBJECT标记(表示开始)。接着调用writeClassDesc方法写入当前对象所属类的类描述信息:
writeClassDesc方法主要用于判断当前的类描述符使用什么方式写入,如果传入的类描述信息是一个null引用,那么会调用writeNull方法,如果没有使用unshared方式,并且可以在handles对象池中找到传入的对象信息,那么调用writeHandle,如果传入的类是一个动态代理类,那么调用writeProxyDesc方法,如果上面三个条件都不满足,那么调用writeNonProxyDesc方法
跟进writeNonProxyDesc方法。首先bout写入TC_CLASSDESC标记(表示新类的描述信息);
1 | private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared) |
进入writeClassDescriptor(desc),会调用writeNonProxy方法,跟进:
1 | void writeNonProxy(ObjectOutputStream out) throws IOException { |
- 先调用writeUTF方法写入类名到字节流,这里的类名是类全名,带了包名的那种(out.writeUTF(name);)
- 再调用writeLong方法写入serialVersionUID的值到字节流(out.writeLong(getSerialVersionUID());)
- 然后开始写入当前类中成员属性的数量信息到字节流(out.writeShort(fields.length);)
- 最后如下图所示,会写入每一个字段的信息,这里的字段信息包含三部分内容:TypeCode、fieldName、fieldType
回到writeNonProxyDesc方法。往bout里写入TC_ENDBLOCKDATA标记(表示一个数据块的结束);
到这里,writeNonProxy和writeClassDescriptor流程结束,同样,也导致writeClassDesc流程结束,并且回到writeOrdinaryObject方法。
首先根据unshared模式来判断是否将desc所表示的类元数据信息插入到handles对象的映射表中;然后,判断传入对象是否实现了外部化并且是动态代理类,并因此调用不同的方法写入对象信息:
跟进writeSerialData方法,该方法主要向obj对象写入数据信息,比如字段值和相关引用等,写入的时候会从顶级父类从上至下递归执行。
1 | private void writeSerialData(Object obj, ObjectStreamClass desc) |
在序列化传入对象之前,先从类描述信息中获取ClassDataSlot信息,在得到继承结构后,开始遍历:
先判断传入对象的类是否重写了writeObject方法,有则调用该重写方法,然后bout写入TC_ENDBLOCKDATA标记。
如果没有重写writeObject方法,则直接调用defaultWriteFields方法,该方法负责读取 obj 对象中的字段数据(desc),并且将字段数据写入到字节流中。
还是先用checkDefaultSerialize()检查一遍当前对象是否可序列化,如果该对象不可序列化,抛出newInvalidClassException异常。
先获取该对象中所有基础类型字段的值:
跟进到getPrimFieldValues方法:
按照Person类的字段,首先是int类型的age:
然后是String类型name,不是基础类型,直接结束流程。
再获取非基础类型的字段的值:
跟进到getObjFieldValues方法,流程类似:
在Java序列化中,符号[和L分别代表:
- [:表示数组类型。例如,[I表示一个整数数组,[Ljava/lang/String;表示一个字符串对象数组。
- L:表示一个对象类型。L后面跟着对象的完全限定名,例如,Ljava/lang/String;表示一个字符串对象。
然后再进入一次writeObject0方法,在这个方法里写入对象类型的字段的值,最终完成序列化操作。其实还是一个嵌套的过程。
到这里,序列化流程也就结束了。
囿于能力有限,笔者也是草草分析。