单例模式安全之序列化攻击

单例模式安全之序列化攻击 源码
什么是序列化攻击呢? 简单说,一个单例对象经过序列化再反序列化后,内存中会存在两个对象,这样单例模式就被破坏。
序列化攻击复现

序列化攻击复过程
  1. 获取到单例对象
  2. 对象序列化持久到磁盘
  3. 反序列化成对象
这里采用JDK的自带的序列化方式
单例实现Serializable接口
package com.fine.serialize; import java.io.Serializable; /** * 单例 * volatile 双重校验 * * @author finefine at: 2019-05-03 21:43 */ public class Singleton implements Serializable {private static final long serialVersionUID = 1L; private volatile static Singleton INSTANCE; private Singleton() {}public static Singleton getInstance() {if (INSTANCE==null){//同步代码块 synchronized (Singleton.class){ if (INSTANCE == null) { INSTANCE = new Singleton(); } }} return INSTANCE; } }

测试代码
package com.fine.serialize; import java.io.*; /** * @author finefine at: 2019-05-03 21:50 */ public class DeSerailizeAttackTest {public static void main(String[] args) { Singleton singleton = Singleton.getInstance(); try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("object")); ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("object"))) {//将对象持久化到磁盘中 outputStream.writeObject(singleton); outputStream.flush(); //从磁盘中反序列化成对象 Singleton singleton1 = (Singleton) inputStream.readObject(); if (singleton == singleton1) { System.out.println("是同一个对象"); } else { System.out.println("是两个不同的对象"); }} catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }

结果输出了两个不同的对象
单例模式安全之序列化攻击
文章图片
image-20190503221321745 通过debug 可以看到确实是两个不同的对象
单例模式安全之序列化攻击
文章图片
image-20190503221436080 反序列化攻击源码分析 反序列化攻击的问题代码在此
//默认情况下 该方法重新new对象 private Object readOrdinaryObject(boolean unshared) throws IOException { if (bin.readByte() != TC_OBJECT) { throw new InternalError(); }ObjectStreamClass desc = readClassDesc(false); desc.checkDeserialize(); Class cl = desc.forClass(); if (cl == String.class || cl == Class.class || cl == ObjectStreamClass.class) { throw new InvalidClassException("invalid class descriptor"); }Object obj; try { obj = desc.isInstantiable() ? desc.newInstance() : null; } catch (Exception ex) { throw (IOException) new InvalidClassException( desc.forClass().getName(), "unable to create instance").initCause(ex); }passHandle = handles.assign(unshared ? unsharedMarker : obj); ClassNotFoundException resolveEx = desc.getResolveException(); if (resolveEx != null) { handles.markException(passHandle, resolveEx); }if (desc.isExternalizable()) { readExternalData((Externalizable) obj, desc); } else { readSerialData(obj, desc); }handles.finish(passHandle); //经过上面的代码,新对象已经被new 出来了, hasReadResolveMethod这个方法很关键 //下面的逻辑就是说 如果该类存在一个readResolve 方法就会调用该方法,并重新替换新的对象,如果不存在就直接把new出来的对象返回出去 if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj); if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } if (rep != obj) { // Filter the replacement object if (rep != null) { if (rep.getClass().isArray()) { filterCheck(rep.getClass(), Array.getLength(rep)); } else { filterCheck(rep.getClass(), -1); } } handles.setObject(passHandle, obj = rep); } }return obj; }/** * * 返回该类是否有readResolve方法 */ boolean hasReadResolveMethod() { requireInitialized(); return (readResolveMethod != null); }

由于Singleton 类中不存在readResolve ,所以也就导致反序列化出新的对象了。
解决方法
  1. 添加readResolve方法
  2. 使用枚举类
添加readResolve
Singleton 代码
package com.fine.serialize; import java.io.Serializable; /** * 单例 * volatile 双重校验 * * @author finefine at: 2019-05-03 21:43 */ public class Singleton implements Serializable {private static final long serialVersionUID = 1L; private volatile static Singleton INSTANCE; private Singleton() {}public static Singleton getInstance() {if (INSTANCE==null){//同步代码块 synchronized (Singleton.class){ if (INSTANCE == null) { INSTANCE = new Singleton(); } }} return INSTANCE; }//添加的readResolve方法 private Object readResolve() { return INSTANCE; } }

这里测试代码不用更改,看看测试结果
单例模式安全之序列化攻击
文章图片
image-20190503225944248 使用枚举类
单例代码
package com.fine.serialize; import java.io.Serializable; /** * @author finefine at: 2019-05-03 23:00 */ public enumSingletonEnum implements Serializable { INSTANCE; private static final long serialVersionUID = 2L; }

测试代码
package com.fine.serialize; import java.io.*; /** * @author finefine at: 2019-05-03 23:02 */ public class DeSerailizeEnumAttackTest {public static void main(String[] args) { SingletonEnum singleton = SingletonEnum.INSTANCE; try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("object")); ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("object"))) {//将对象持久化到磁盘中 outputStream.writeObject(singleton); outputStream.flush(); //从磁盘中反序列化成对象 SingletonEnum singleton1 = (SingletonEnum) inputStream.readObject(); if (singleton == singleton1) { System.out.println("是同一个对象"); } else { System.out.println("是两个不同的对象"); }} catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }

测试结果
单例模式安全之序列化攻击
文章图片
image-20190504000713071 那为什么这里使用枚举类就可以避免反射攻击呢?深入源码分析
JDK 的反序列化都会调用到这个方法,readObject0对每种类型反序列化都做了不同的实现,当对枚举类进行反序列化时进入到TC_ENUM分支,最终调用readEnum方法
private Object readObject0(boolean unshared) throws IOException { boolean oldMode = bin.getBlockDataMode(); if (oldMode) { int remain = bin.currentBlockRemaining(); if (remain > 0) { throw new OptionalDataException(remain); } else if (defaultDataEnd) { /* * Fix for 4360508: stream is currently at the end of a field * value block written via default serialization; since there * is no terminating TC_ENDBLOCKDATA tag, simulate * end-of-custom-data behavior explicitly. */ throw new OptionalDataException(true); } bin.setBlockDataMode(false); }byte tc; while ((tc = bin.peekByte()) == TC_RESET) { bin.readByte(); handleReset(); }depth++; totalObjectRefs++; try { switch (tc) { case TC_NULL: return readNull(); case TC_REFERENCE: return readHandle(unshared); case TC_CLASS: return readClass(unshared); case TC_CLASSDESC: case TC_PROXYCLASSDESC: return readClassDesc(unshared); case TC_STRING: case TC_LONGSTRING: return checkResolve(readString(unshared)); case TC_ARRAY: return checkResolve(readArray(unshared)); //枚举类 case TC_ENUM: return checkResolve(readEnum(unshared)); //Object case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); case TC_EXCEPTION: IOException ex = readFatalException(); throw new WriteAbortedException("writing aborted", ex); case TC_BLOCKDATA: case TC_BLOCKDATALONG: if (oldMode) { bin.setBlockDataMode(true); bin.peek(); // force header read throw new OptionalDataException( bin.currentBlockRemaining()); } else { throw new StreamCorruptedException( "unexpected block data"); }case TC_ENDBLOCKDATA: if (oldMode) { throw new OptionalDataException(true); } else { throw new StreamCorruptedException( "unexpected end of block data"); }default: throw new StreamCorruptedException( String.format("invalid type code: %02X", tc)); } } finally { depth--; bin.setBlockDataMode(oldMode); } }

readEnum 最终调用了Enum.valueOf 返回实例对象
private Enum readEnum(boolean unshared) throws IOException { if (bin.readByte() != TC_ENUM) { throw new InternalError(); }ObjectStreamClass desc = readClassDesc(false); if (!desc.isEnum()) { throw new InvalidClassException("non-enum class: " + desc); }int enumHandle = handles.assign(unshared ? unsharedMarker : null); ClassNotFoundException resolveEx = desc.getResolveException(); if (resolveEx != null) { handles.markException(enumHandle, resolveEx); }String name = readString(false); Enum result = null; Class cl = desc.forClass(); if (cl != null) { try { @SuppressWarnings("unchecked") //这里找到了实例 Enum en = Enum.valueOf((Class)cl, name); result = en; } catch (IllegalArgumentException ex) { throw (IOException) new InvalidObjectException( "enum constant " + name + " does not exist in " + cl).initCause(ex); } if (!unshared) { handles.setObject(enumHandle, result); } }handles.finish(enumHandle); passHandle = enumHandle; return result; }

总结 【单例模式安全之序列化攻击】经过单例模式安全之反射攻击和本片文章的内容,可以发现,使用枚举类实现单例是非常有利的,不用开发者考虑太多其他的因素。从单例的角度及安全的角度来看,枚举单例模式有以下三个特点:
  • jvm 底层保证线程安全。
  • jvm 底层抑制了反射攻击。
  • jdk 序列化方式的特殊处理,防止了反序列化攻击。

    推荐阅读