转载

Java序列化机制

序列化指的是将对象编码为字节流、反序列化指的是将字节流重新构建为对象

用途

  • RMI(Remote Method Invoke) 远程方法调用
  • JMX
  • JMS

如何使用Java默认的序列化能力

  • 类要实现Serializable接口,并且定义版本号(字段serialVersionUID),不定义版本号的话系统会对类的结构运用一个加密的散列函数,这个自动产生的值会受到类名称、实现的接口名称、所有公有和受保护的成员的名称所影响。==实现Serializable接口的类,为了保持兼容性,要显式定义版本号==
  • 单例的类需要定义readResolve方法
  • 定义readObject、writeObject来自定义序列化(与关键字transient结合使用)

默认序列化的劣势

  • 消耗过多空间(ArrayList自定义了序列化方法避免了这个问题)
  • 反序列化的时候可能会破坏了构造器中对参数的约束、对必要的参数进行保护性拷贝这两个原则

隐患

将不被信任的字节流进行反序列化,可能导致

  • 远程代码执行
  • 拒绝服务

一个字节码攻击的例子

如果系统中的执行结果依赖于某个类的可信任状态(Period中start、end的final特性,以及其构造函数内部对参数进行正确性校验),利用字节码攻击的手段可以影响start、end这两个参数的正确性。从而使依赖于Period的代码受到污染。客户端代码持有了Period实例域的引用,在客户端中可以随意修改该引用的状态。要避免这种攻击必须自定义序列化逻辑,对实例域进行保护性拷贝,确保Period类的约束条件不被破坏

  • 系统中存在一个可被序列化的类

    package com.luhc;
    
    import java.io.Serializable;
    import java.util.Date;
    
    /**
     * 内部维持了两个final的实例域
     *
     * @author luhuancheng
     * @date 2019/3/17
     */
    public final class Period implements Serializable {
        private final Date start;
        private final Date end;
    
        public Period(Date start, Date end) {
            // 保护性拷贝final属性
            this.start = new Date(start.getTime());
            this.end = new Date(end.getTime());
    
            // 检验参数
            if (start.compareTo(end) > 0) {
                throw new IllegalArgumentException(start + " after " + end);
            }
        }
    
        public Date getStart() {
            // 保护性拷贝后,返回一个新的对象,避免被外部改变了内部的状态
            return new Date(start.getTime());
        }
    
        public Date getEnd() {
            // 保护性拷贝后,返回一个新的对象,避免被外部改变了内部的状态
            return new Date(end.getTime());
        }
    
        @Override
        public String toString() {
            return "Period{" +
                    "start=" + start +
                    ", end=" + end +
                    '}';
        }
    }
    
  • 一个对字节码进行伪造的攻击类

    package com.luhc;
    
    import java.io.*;
    import java.util.Date;
    
    /**
     * @author luhuancheng
     * @date 2019/3/17
     */
    public class AttackPeriod {
    
        public final Period period;
    
        public final Date start;
    
        public final Date end;
    
        public AttackPeriod() {
            try {
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                ObjectOutputStream out = new ObjectOutputStream(bos);
                // 写入对象字节流
                out.writeObject(new Period(new Date(), new Date()));
    
                // 伪造字节流,之后我们可以从字节流中直接读取到Period对象中的两个实例域(start、end),这意味着我们能够修改这两个本应该是final的实例域的状态
                byte[] ref = {0x71, 0, 0x7e, 0, 5};
                bos.write(ref);   // start
                ref[4] = 4;
                bos.write(ref);   // end
    
                // 将字节流反序列为对象
                ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
                period = (Period) in.readObject();
                // 从字节流中反序列化Period两个final的实例域,指向AttackPeriod的公开实例域,之后可以修改这两个指针所指的对象
                // 此时的Period实例域start、end与AttackPeriod中两个实例域start、end指向的是内存中同一个对象
                start = (Date) in.readObject();
                end = (Date) in.readObject();
            } catch (IOException | ClassNotFoundException e) {
                throw new AssertionError(e);
            }
        }
    
        public static void main(String[] args) {
            AttackPeriod ap = new AttackPeriod();
            Period p = ap.period;
            Date pEnd = ap.end;
    
            pEnd.setYear(76);
            System.out.println(p);
    
        }
    }
    

如何避免隐患

  • 使用结构化数据表示法(JSON、Protobuf等) 序列化方案对比
  • 序列化代理
  • 自定义readObject,在其中进行参数校验和必要的数据保护性拷贝

优化Period类,自定义序列化逻辑

自定义readObject逻辑(进行数据的保护性拷贝、参数校验),避免字节码攻击

package com.luhc;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Date;

/**
 * 内部维持了两个final的实例域
 *
 * @author luhuancheng
 * @date 2019/3/17
 */
public final class Period implements Serializable {
    // 在readObject中对这两个实例域进行保护性拷贝 赋值,因此无法再保持final特性
    private Date start;
    private Date end;

    public Period(Date start, Date end) {
        // 保护性拷贝final属性
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());

        // 检验参数
        if (start.compareTo(end) > 0) {
            throw new IllegalArgumentException(start + " after " + end);
        }
    }

    public Date getStart() {
        // 保护性拷贝后,返回一个新的对象,避免被外部改变了内部的状态
        return new Date(start.getTime());
    }

    public Date getEnd() {
        // 保护性拷贝后,返回一个新的对象,避免被外部改变了内部的状态
        return new Date(end.getTime());
    }
    
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        // 序列化规范必须的步骤
        in.defaultReadObject();
        
        // 保护性拷贝(start、end不能使用final修饰了)
        start = new Date(start.getTime());
        end = new Date(end.getTime());
        
        // 参数校验
        if (start.compareTo(end) > 0) {
            throw new InvalidObjectException(start + " after " + end);
        }
    }

    @Override
    public String toString() {
        return "Period{" +
                "start=" + start +
                ", end=" + end +
                '}';
    }
}

优化Period、使用序列化代理来自定义序列化逻辑

在上一种优化处理中,在readObject方法内部需要修改start、end的值,因此这两个实例域不能再被final修饰。使用序列化代理可以维持实例域依旧是final的

package com.luhc;

import java.io.Serializable;
import java.util.Date;

/**
 * 内部维持了两个final的实例域
 *
 * @author luhuancheng
 * @date 2019/3/17
 */
public final class Period implements Serializable {
    
    // 不可变类的内部实例域,使用final修饰确保该类真正是不可变的
    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        // 保护性拷贝final属性
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());

        // 检验参数
        if (start.compareTo(end) > 0) {
            throw new IllegalArgumentException(start + " after " + end);
        }
    }

    public Date getStart() {
        // 保护性拷贝后,返回一个新的对象,避免被外部改变了内部的状态
        return new Date(start.getTime());
    }

    public Date getEnd() {
        // 保护性拷贝后,返回一个新的对象,避免被外部改变了内部的状态
        return new Date(end.getTime());
    }

    @Override
    public String toString() {
        return "Period{" +
                "start=" + start +
                ", end=" + end +
                '}';
    }
    
    // 序列化代理
    // 1. 定义代理内部类
    private static class SerializationProxy implements Serializable {
        private final Date start;
        private final Date end;

        // 构造器传入外围类的实例
        public SerializationProxy(Period period) {
            this.start = period.start;
            this.end = period.end;
        }
        
        // 3. 定义readResolve方法,返回外围类实例
        private Object readResolve() {
            return new Period(start, end);
        }
    }
    
    // 2. 外围类定义writeReplace方法, 返回代理类
    private Object writeReplace() {
        return new SerializationProxy(this);
    }
}
原文  https://blog.luhuancheng.com/2019/03/18/java中的序列化机制/
正文到此结束
Loading...