4-Web安全——java序列化机制分析
Posted songly_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了4-Web安全——java序列化机制分析相关的知识,希望对你有一定的参考价值。
本文是根据java的序列化机制来分析java序列化的流程,从而理解序列化后res文件中的字符串具体含义(看不懂的同学建议先看完《java的序列化机制》)。
通过前面的学习不难发现其实整个序列化过程就两行代码:
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(student);
本次分析是以实现Serializable接口的序列化方式(默认的java序列化方式),当实例化objectOutputStream 对象时,会调用单个参数的构造方法传入fileOutputStream进行一些序列化的初始化工作。
public ObjectOutputStream(OutputStream out) throws IOException {
//校验
verifySubclass();
//初始化成员属性
bout = new BlockDataOutputStream(out);
handles = new HandleTable(10, (float) 3.00);
subs = new ReplaceTable(10, (float) 3.00);
enableOverride = false;
writeStreamHeader();
bout.setBlockDataMode(true);
if (extendedDebugInfo) {
debugInfoStack = new DebugTraceInfoStack();
} else {
debugInfoStack = null;
}
}
调用ObjectOutputStream构造之前,会先调用verifySubclass方法,其作用是实例化ObjectOutputStream之前先对其进行一些验证性处理,ObjectOutputStream的内部类主要是定义了常用的序列化方法(write前缀方法),接着初始化成员属性:bout、handles、subs、enableOverride。初始化完成员属性后,调用writeStreamHeader方法写入魔数和序列化版本信息到缓冲区。
verifySubclass方法内部会调用getClass方法如果返回当前class类型,如果是ObjectOutputStream则直接返回。
private void verifySubclass() {
Class<?> cl = getClass();
if (cl == ObjectOutputStream.class) {
//通常会在此处返回
return;
}
//省略
}
ObjectOutputStream初始化了几个成员属性,这里解释一下:
subs:也是一个哈希表,表示从对象到“替换对象”的一个映射关系
handler:是一个哈希表,表示从对象到引用的映射(上一篇已经介绍过引用的概念,这里不再赘述)
enableOverride:主要是判断是否重写了writeObject方法,enableOverride是一个boolean类型,true表示重写了writeObject方法
bout:可以理解为一个“容器”,它用于构造一个BlockDataOutputStream处理数据块转换的过滤流,也就是将缓冲区中的序列化数据写入字节流
调用writeStreamHeader方法写入魔数和序列化的版本
protected void writeStreamHeader() throws IOException {
bout.writeShort(STREAM_MAGIC);
bout.writeShort(STREAM_VERSION);
}
再调用setBlockDataMode方法开启输出流的Data Block模式(主要是为了解决JDK 1.2和JDK 1.1不兼容的字节流格式问题),然后调用writeObject方法开始写入序列化数据
public final void writeObject(Object obj) throws IOException {
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}
从writeObject方法的内部实现中可以看到enableOverride的值如果为true的话说明重写了writeObject方法,那么序列化时会调用writeObjectOverride方法,而ObjectOutputStream类默认是调用writeObject写入序列化数据的,从前面的分析中可以看到ObjectOutputStream的构造通常会将enableOverride设置为false。
补充一下Data Block模式,java对象序列化后会永久存储在磁盘上,操作系统的文件系统的最小单位就是数据块(Block),因此序列化后的数据也是以数据块的形式存储在磁盘中,但数据实际上并不会直接写入到磁盘,一般会先写入到操作系统的内核缓冲区中,再由缓冲区写入到磁盘实现数据的持久化存储。
接着又调用了writeObject0核心方法,并且第二个参数传入了false,说明使用的Java对象序列化方式不是unshared方式,这个方法是ObjectOutputStream中用于写入对象信息的核心方法(通常会在writeObject方法中来调用)
private void writeObject0(Object obj, boolean unshared) throws IOException {
//关闭了输出流的Data Block模式,将oldMode设置为原始模式
boolean oldMode = bout.setBlockDataMode(false);
depth++;
try {
// handle previously written and non-replaceable objects
int h;
//如果当前传入对象在“替换哈希表”(ReplaceTable)中无法找到,则调用writeNull方法并且返回
if ((obj = subs.lookup(obj)) == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;
//对否为特殊类型Class
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
//传入对象是否为特殊类型ObjectStreamClass
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
// check for replacement object
//检查替换对象,
Object orig = obj;
Class<?> cl = obj.getClass();
ObjectStreamClass desc;
//通过循环查找“替换对象”
for (;;) {
// REMIND: skip this check for strings/arrays?
Class<?> repCl;
desc = ObjectStreamClass.lookup(cl, true);
//跳过string/arrays类型
if (!desc.hasWriteReplaceMethod() ||
(obj = desc.invokeWriteReplace(obj)) == null ||
(repCl = obj.getClass()) == cl)
{
break;
}
cl = repCl;
}
//是否启用了替换功能(Replace功能)
if (enableReplace) {
Object rep = replaceObject(obj);
if (rep != obj && rep != null) {
cl = rep.getClass();
desc = ObjectStreamClass.lookup(cl, true);
}
obj = rep;
}
// if object replaced, run through original checks a second time
//如果对象被替换,再次检查原始对象
if (obj != orig) {
subs.assign(orig, obj);
if (obj == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
}
// remaining cases
//继续处理其他对象类型,然后调用对应的方法
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
//是否为可序列化的object类型
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
//如果以上都不满足,则抛出异常
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
} finally {
depth--;
//将Data Block模式还原
bout.setBlockDataMode(oldMode);
}
}
这里所谓的替换对象是在可序列化方法中重写了writeReplace方法的java对象,但通常enableReplace的值为false,不会开启“替换”功能。因此通常程序会往下走,继续处理其他对象类型,根据不同的对象类型然后调用对应的write前缀方法写入数据到字节流,是否为可序列化的object类型。
“unshared”(非共享)方式是java对象序列化的一种方式,如果使用unshared方式序列化,writeObject方法每次写入对象都会以一个新对象写入到字节流中,也就是当做一个新对象来处理。到这我们基本可以明白,Serializable接口序列化默认是unshared方式。
ObjectOutputStream类中有许多定义的write*前缀的私有方法,不光有writeObject方法,还有八大基础数据类型的write前缀方法,用于写入不同java对象到字节流中。
这里不全部分析,以writeString函数(String对象)为例,调用writeObject方法写入一个“hello world”字符串对象
objectOutputStream.writeObject("hello world");
再开启debug调试,如下所示
writeString首先判断当前写入方式是否为unshared,如果不是则调用getUTFLength函数获取String字符串的长度,并判断String字符串长度是否大于0xFFFF,这里肯定不大于,接着写入TC_STRING标记,然后再写入字符串和长度。
writeString方法分析完毕,现在我们再来重点分析writeOrdinaryObject方法
private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) throws IOException {
if (extendedDebugInfo) {
debugInfoStack.push(
(depth == 1 ? "root " : "") + "object (class \\"" +
obj.getClass().getName() + "\\", " + obj.toString() + ")");
}
try {
//是否可序列化
desc.checkSerialize();
//写入TC_OBJECT标记
bout.writeByte(TC_OBJECT);
//写入当前对象所属类的类描述信息
writeClassDesc(desc, false);
//是否为unshared方式
handles.assign(unshared ? null : obj);
if (desc.isExternalizable() && !desc.isProxy()) {
//使用了动态代理类和实现了外部化
writeExternalData((Externalizable) obj);
} else {
//实现了Serializable接口
writeSerialData(obj, desc);
}
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
writeOrdinaryObject方法跟writeString有所不同,多了一个desc参数,其类型为ObjectStreamClass,通过查看源码发现ObjectStreamClass是一个实现了Serializable接口的类
ObjectStreamClass类主要作用是用于在序列化过程中提取对象的类描述信息(成员属性),可以理解为ObjectStreamClass就是一个存储可序列化对象的“容器”(数组),换句话说,ObjectStreamClass也是具有可序列化语义的。
现在我们回到writeOrdinaryObject方法中,参数desc调用了checkSerialize方法检查当前对象是否为一个可序列化对象,如果不具备可序列化则抛出InvalidClassException异常
void checkSerialize() throws InvalidClassException {
requireInitialized();
if (serializeEx != null) {
throw serializeEx.newInvalidClassException();
}
}
接着写入TC_OBJECT标记,然后调用writeClassDesc函数写入当前对象的类描述信息。
private void writeClassDesc(ObjectStreamClass desc, boolean unshared) throws IOException {
int handle;
//判断类描述信息的引用是否为null
if (desc == null) {
writeNull();
//是否为“非共享”序列化,handlers对象池是否有传入的对象信息
} else if (!unshared && (handle = handles.lookup(desc)) != -1) {
writeHandle(handle);
//是否为动态代理类
} else if (desc.isProxy()) {
writeProxyDesc(desc, unshared);
} else {
writeNonProxyDesc(desc, unshared);
}
}
该方法主要用于判断当前的类描述符使用什么方式写入,writeProxyDesc与writeString方法较为类似,writeProxyDesc方法这里暂不分析,在以上条件都不满足的情况下会调用writeNonProxyDesc方法。
writeNonProxyDesc方法内部实现:
private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared) throws IOException{
//写入TC_CLASSDESC标记
bout.writeByte(TC_CLASSDESC);
//判断序列化方式
handles.assign(unshared ? null : desc);
//判断使用的序列化字节流协议版本信息
if (protocol == PROTOCOL_VERSION_1) {
// do not invoke class descriptor write hook with old protocol
desc.writeNonProxy(this);
} else {
writeClassDescriptor(desc);
}
Class<?> cl = desc.forClass();
//开启Data Block模式
bout.setBlockDataMode(true);
if (cl != null && isCustomSubclass()) {
ReflectUtil.checkPackageAccess(cl);
}
annotateClass(cl);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
writeClassDesc(desc.getSuperDesc(), false);
}
首先写入TC_CLASSDESC标记表示写入非动态代理类的类描述信息,如果写入模式是unshared方式,将desc所表示的类描述信息插入到handles对象的映射表中。并根据使用的序列化字节流协议版本号调用不同的write前缀方法:writeNonProxy和writeClassDescriptor方法,接着开启Data Block模式。
接着调用annotateClass方法,但annotateClass方法内部什么也没做,调用该方法之后,ObjectOutputStream类可以自由写入任何格式的字节流类信息(annotateClass方法默认只会被调用一次)。然后关闭Data Block模式,写入TC_ENDBLOCKDATA标记作为当前非动态代理类的类描述信息的结束标记,writeClassDesc函数以非unshared方式写入当前对象所属类的父类的类描述信息。
这两个write前缀方法比较重要,先来看writeClassDescriptor方法:
protected void writeClassDescriptor(ObjectStreamClass desc) throws IOException{
desc.writeNonProxy(this);
}
writeClassDescriptor方法内部调用了writeNonProxy方法。
再来看writeNonProxy方法
void writeNonProxy(ObjectOutputStream out) throws IOException {
//写入类全名,suid
out.writeUTF(name);
out.writeLong(getSerialVersionUID());
byte flags = 0;
//是否实现了externalizable接口
if (externalizable) {
flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;
int protocol = out.getProtocolVersion();
if (protocol != ObjectStreamConstants.PROTOCOL_VERSION_1) {
flags |= ObjectStreamConstants.SC_BLOCK_DATA;
}
//是否实现了serializable接口
} else if (serializable) {
flags |= ObjectStreamConstants.SC_SERIALIZABLE;
}
if (hasWriteObjectData) {
flags |= ObjectStreamConstants.SC_WRITE_METHOD;
}
if (isEnum) {
flags |= ObjectStreamConstants.SC_ENUM;
}
//写入SC_SERIALIZABLE标记
out.writeByte(flags);
//写入类成员属性的数量
out.writeShort(fields.length);
//依次写入每个成员属性
for (int i = 0; i < fields.length; i++) {
ObjectStreamField f = fields[i];
out.writeByte(f.getTypeCode());
out.writeUTF(f.getName());
if (!f.isPrimitive()) {
out.writeTypeString(f.getTypeString());
}
}
}
writeUTF方法用于写入类名到字节流,这里的类名是带了包名的类全名,然后调用writeLong方法写入suid的值到字节流,判断当前对象是否实现了serializable还是externalizable接口,如果实现了serializable接口写入SC_SERIALIZABLE标记(表示这是一个可序列化对象),调用writeShort方法写入当前类中成员属性的数量信息到字节流,最后写入每一个字段的信息:类型代码(TypeCode)、字段名(fieldName)、字段类型字符串(fieldType)。
再返回到writeOrdinaryObject方法中,继续往下分析:
如果使用的模式是非共享模式,则将desc所表示类的类描述信息插入到handles对象映射表中,接着判断当前对象的序列化方式,调用对应的write前缀方法。
如果实现了Serializable接口就调用writeSerialData方法
private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException {
//首先获取
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
//是否重写writeObject方法
if (slotDesc.hasWriteObjectMethod()) {
PutFieldImpl oldPut = curPut;
curPut = null;
SerialCallbackContext oldContext = curContext;
if (extendedDebugInfo) {
debugInfoStack.push(
"custom writeObject data (class \\"" +
slotDesc.getName() + "\\")");
}
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bout.setBlockDataMode(true);
slotDesc.invokeWriteObject(obj, this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
} finally {
curContext.setUsed();
curContext = oldContext;
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
curPut = oldPut;
} else {
//没有重写writeObject方法
defaultWriteFields(obj, slotDesc);
}
}
}
该方法主要负责写入java对象数据信息,比如字段值和相关引用等,写入的时候会从顶级父类从上至下递归执行。
序列化之前,调用getClassDataLayout方法获取ClassDataSlot信息(其实就是获取“ObjectStreamClass容器”)
在得到继承结构后开始遍历,然后判断可序列化对象如果重写了writeObject方法,先保存当前上下文,开启Data Block模式后invokeWriteObject方法会调用重写后的“writeObject”方法,参数obj表示重写了writeObject方法的类对象,out表示重写的writeObject方法的参数,再关闭Data Block模式,写入TC_ENDBLOCKDATA标记,最后还原之前的上下文。如果没有重写writeObject方法就调用defaultWriteFields方法写入当前序列化对象的所有字段信息。
跟进defaultWriteFields方法,这里传入的obj对象就是Student对象
private void defaultWriteFields(Object obj, ObjectStreamClass desc) throws IOException {
Class<?> cl = desc.forClass();
if (cl != null && obj != null && !cl.isInstance(obj)) {
throw new ClassCastException();
}
//检查是否可序列化
desc.checkDefaultSerialize();
int primDataSize = desc.getPrimDataSize();
if (primVals == null || primVals.length < primDataSize) {
primVals = new byte[primDataSize];
}
//获取基本数据类型字段
desc.getPrimFieldValues(obj, primVals);
//以非unshared方式,写入基本数据类型到字节流缓冲区
bout.write(primVals, 0, primDataSize, false);
ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
//获取对象类型字段
desc.getObjFieldValues(obj, objVals);
//循环依次写入对象类型字段
for (int i = 0; i < objVals.length; i++) {
if (extendedDebugInfo) {
debugInfoStack.push(
"field (class \\"" + desc.getName() + "\\", name: \\"" +
fields[numPrimFields + i].getName() + "\\", type: \\"" +
fields[numPrimFields + i].getType() + "\\")");
}
try {
writeObject0(objVals[i],
fields[numPrimFields + i].isUnshared());
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
}
checkDefaultSerialize方法检查是否可序列化,getPrimFieldValues方法获取当前对象中所有基础类型字段的内容写入到字节流,getObjFieldValues方法获取对象类型字段的内容,然后writeObject0方法以非unshared方式写入对象类型字段数据。也就是说getPrimFieldValues方法会获取student对象的id,score基本数据类型的成员属性,而getObjFieldValues方法会获取student对象的name成员属性(name的数据类型String属于对象类型,需要注意的是getObjFieldValues方法会获取所有对象类型的成员属性),而address成员属性由于被transient关键字修饰,具有不可序列化语义,不会参与序列化过程。
本次序列化流程分析完毕,现在再回过头阅读res文件,应该不在话下。
本次序列化流程分析主要参考了这位大佬的文章,这位大佬对java序列化与反序列化的研究深度令人惊叹,花了几天时间学习,对java的序列化和反序列化理解加深了不少,附上大佬的链接:
https://blog.csdn.net/silentbalanceyh/article/details/8294269
以上是关于4-Web安全——java序列化机制分析的主要内容,如果未能解决你的问题,请参考以下文章