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序列化机制分析的主要内容,如果未能解决你的问题,请参考以下文章

5-java安全——java反序列化机制分析

5-Web安全——java反序列化机制分析

Java安全之反序列化漏洞分析

3-java安全——java序列化机制

3-Web安全——java的序列化机制

12-java安全——java反序列化CC7链分析