Java序列化安全机制
前言 上一篇最后我们提到采用java默认的序列化机制是存在安全漏洞的。第一种漏洞就是在网络中传播二进制流时被黑客截获,获取其中的一些敏感信息,比如账号密码以及上文提到的苹果的价格;另一种就是黑客截获到了这些信息后加以修改,再通过网络发送出去,比如恶意修改了苹果价格信息,那么销售商将会面临破产的危机。基于此此,java提供自定义序列化机制来避免第一种漏洞;采用反序列化的验证机制来避免第二种漏洞。另外,一定要理解上一篇文章中提到的序列化流的格式,即分为三部分:序列化头信息部分、类的描述部分以及属性域的值部分,下文中会多次提到。 自定义序列化 所谓java自定义序列化, java提供了三种实现方案,前两种方案实际上是自定义第三部分信息(属性域的值部分)的输出方式,而第三种方法不仅可以自定义第三部分信息的输出,还可以自定义第二部分信息(类描述部分)信息的输出。第一种方案采用默认机制与自定义机制相结合的方法;第二种是完全自定义序列化属性值的方法,不仅可以有选择的储存本对象包含的数据,还可以存储其他非this对象包含的数据。这两种自定义的程度还只是停留在定制对象内部属性的描述,而对于序列化对象本身的描述无法定制,这时我们可以采用第三种方案:新建一个自己的序列化类来实现。 java.io.Serializable自定义形式 这种自定义序列化方案就是有选择的序列化对象域,而不是把对象的所有域内容都序列化。通过在成员变量上添加transient关键字,我们可以不让默认的序列化机制序列化该成员(比如苹果价格),我们可以将该成员加密后手动的写入到序列化流中。java.io.Serializable接口自定义序列化的核心是:在要序列化的类中添加如下签名方式的两个方法: --------------------------------------------------------------------------------------------------------------------------------- private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException; private void writeObject(java.io.ObjectOutputStream stream) throws IOException --------------------------------------------------------------------------------------------------------------------------------- 改写后的Apple类如下: --------------------------------------------------------------------------------------------------------------------------------- package com.fnst.infoQ; public class Apple extends Fruit{ private String name; private transient int price; public Apple(String _name,int _price){ super(); this.name = _name; this.price = _price; } public Apple() {this.name = "Default Apple"; price = 4;} private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{ in.defaultReadObject(); //将price解密 int passPrice = in.readInt(); this.price = deciphering(passPrice); } private void writeObject(java.io.ObjectOutputStream out) throws IOException{ out.defaultWriteObject(); //将price加密后,写入流中 int passPrice = encryption(price); out.writeInt(passPrice); } } --------------------------------------------------------------------------------------------------------------------------------- 很多人对ObjcetOutputStream类的writeObject(readObject)与defaultWriteObect(defaultReadObject)方法感到疑惑,不知道如何使用,就是知道如何使用又不清楚为何这样使用。其实比较简单,writeObject(readObject)的作用是序列化(反序列化)后两部分内容,即类的描述部分和属性域的值部分;而defaultWriteObect(defaultReadObject)的作用就是序列化(反序列化)最后一部分内容,即属性域的值部分。JDK中序列化的调用栈与具体代码如下: java.io.ObjectOutputStream.writeObject ()……………………………………………………………………① └java.io.ObjectOutputStream.writeObject0()………………………………………………………………② └java.io.ObjectOutputStream.writeOrdinaryObject()………………………………………③ ├java.io.ObjectOutputStream.writeClassDesc()………………………………………………④ └java.io.ObjectOutputStream.writeSerialData ()…………………………………………⑤ 其中序列化第二部分信息(类的描述部分)由③④完成,而⑤主要完成的就是第三部分信息的序列化,代码如下: --------------------------------------------------------------------------------------------------------------------------------- 1448 private void writeSerialData(Object obj, ObjectStreamClass desc) 1449 throws IOException 1450 { //获取序列化对象由子类到父类的类描述集合 1451 ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); 1452 for (int i = 0; i < slots.length; i++) { 1453 ObjectStreamClass slotDesc = slots[i].desc; //判断序列化类是否实现了writeObject方法 1454 if (slotDesc.hasWriteObjectMethod()) { 1455 PutFieldImpl oldPut = curPut; 1456 curPut = null; -中略- 1464 SerialCallbackContext oldContext = curContext; 1465 try { 1466 curContext = new SerialCallbackContext(obj, slotDesc); 1467 1468 bout.setBlockDataMode(true); //如果实现了wirteObjcet方法,通过反射机制调用该方法 1469 slotDesc.invokeWriteObject(obj, this); 1470 bout.setBlockDataMode(false); 1471 bout.writeByte(TC_ENDBLOCKDATA); 1472 } finally { -中略- 1482 } else { //如果序列化类未实现writeObject方法,调用默认的属性值序列化方式 1483 defaultWriteFields(obj, slotDesc); -下略- --------------------------------------------------------------------------------------------------------------------------------- 上述代码1483行,java.io.FileOutputStream.writeSerialData方法调用的defaultWriteField方法,其实就是defaultWriteObject方法的核心实现。 java.io.FileOutputStream.defaultWriteObject代码如下: --------------------------------------------------------------------------------------------------------------------------------- 415 public void defaultWriteObject() throws IOException { 416 if (curContext == null) { 417 throw new NotActiveException("not in call to writeObject"); 418 } 419 Object curObj = curContext.getObj(); 420 ObjectStreamClass curDesc = curContext.getDesc(); 421 bout.setBlockDataMode(false); //调用默认的属性值序列化方式 422 defaultWriteFields(curObj, curDesc); 423 bout.setBlockDataMode(true); 424 } --------------------------------------------------------------------------------------------------------------------------------- 根据defaultWriteObject的422行代码,可以对writeObject方法和defaultWriteObect方法的作用有了一个清晰的认识。在Java API官方文档描述defaultWrite(Read)Object方法只能从正在序列化的类的 writeObject 方法中调用。如果从其他地方调用该字段,则将抛出 NotActiveException,由defautWriteObject代码的416-418行可以找到答案,curContext是一个描述当前序列化类上下文的类对象(包含序列化类的类描述信息以及具体的类对象),也就是说再未执行③④步骤(将类描述信息序列化到流中,而curContext则代表当前已经序列化到流中的类描述信息上下文)时,是不允许将序列化类的属性值写入到流的。 还有一点需要说明,就是在序列化类中的writeObjcet方法中再次调用out.writeObject(this),程序会不会陷入死循环?代码修改如下: --------------------------------------------------------------------------------------------------------------------------------- private void writeObject(java.io.ObjectOutputStream out) throws IOException{ out.defaultWriteObject(); //将price加密后,写入流中 int passPrice = encryption(price); out.writeInt(passPrice); //添加代码 out.writeObject(this); } --------------------------------------------------------------------------------------------------------------------------------- 答案是否定的,但是如果调用的是out.writeUnshared(this);就会陷入死循环并且最终导致栈溢出异常。其实ObjectOutputStream类的writeObject和writeUnshared方法是基本一致的,只是在写入同一个对象时采用的方式不同。前者采用共享对象的方式(unshared变量为false),同一个对象只可以写入一次;而后者采用非共享的方式(unshared变量为true),同一个可以写入多次。调用栈如下: 1. writeObject方法调用栈 java.io.ObjectOutputStream.writeObject() └java.io.ObjectOutputStream.writeObject0(…,unshared=false) └java.io.ObjectOutputStream.writeOrdinaryObject(…,unshared=false) 2. writeUnshared方法调用栈 java.io.ObjectOutputStream.writeUnshared() └java.io.ObjectOutputStream.writeObject0(…,unshared=true) └java.io.ObjectOutputStream.writeOrdinaryObject(…,unshared=true) 两者的主要区别就是unshared这个布尔 变量的值了。writeOrdinaryObject代码如下: --------------------------------------------------------------------------------------------------------------------------------- 1085 private void writeObject0(Object obj, boolean unshared) 1086 throws IOException 1087 { -中略- 1092 int h; 1093 if ((obj = subs.lookup(obj)) == null) { 1094 writeNull(); 1095 return; //如果该对象是共享的并且该对象之前已经写入,那么将会指定writeHandle方、//法并返回。writeHandle方法的作用就是在流中写入如下4个字节: //0x71 : TC_REFERENCE 该类对象的引用已经在流中 //0x7e0000+类对象在存储栈中的序号 //否则的话会将该对象再次写入到流中 1096 } else if (!unshared && (h = handles.lookup(obj)) != -1) { 1097 writeHandle(h); 1098 return; 1099 } else if (obj instanceof Class) { -下略- --------------------------------------------------------------------- java.io.Externalizable自定义形式 当对象实现了java.io.Externalizable接口时,就可以灵活的控制它的序列化和反序列化过程,该接口继承自java.io.Serializable。Externalizable接口定义了两个方法writeExternal和readExternal。我们的苹果实例可以修改成如下的形式: --------------------------------------------------------------------------------------------------------------------------------- package com.fnst.infoQ; public class Apple extends Fruit implements Externalizable{ private String name; private transient int price; public Apple(String _name,int _price){ super(); this.name = _name; his.price = _price; } public Apple() {this.name = "Default Apple"; price = 4;} public void writeExternal(ObjectOutput out) throws IOException { //可以选择性的将任何类型的域成员值写入到流中 out.writeInt(price); out.writeObject(name); String keyWord = "This is Externalizable test!"; //可以将非this对象包含的数据写入序列化流中 out.writeObject(keyWord); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { this.price = in.readInt(); this.name = (String)in.readObject(); System.out.println(in.readObject()); } } --------------------------------------------------------------------------------------------------------------------------------- Externalizable接口的writeExternal(readExternal)方法的作用仍是序列化第三部分信息(属性域的值部分)。当实现了该接口后,再在该类中添加writeObject(readObjcet)方法,那么writeObject(readObjcet)方法将会失效,具体原因通过阅读源码就了然了。首先调用栈如下: java.io.ObjectOutputStream.writeObject() └java.io.ObjectOutputStream.writeObject0() └java.io.ObjectOutputStream.writeOrdinaryObject() --------------------------------------------------------------------------------------------------------------------------------- 1381 private void writeOrdinaryObject(Object obj, 1382 ObjectStreamClass desc, 1383 boolean unshared) 1384 throws IOException 1385 { -中略- 1397 if (desc.isExternalizable() && !desc.isProxy()) { //序列化实现java.io.Externalizable类对象属性的值 1398 writeExternalData((Externalizable) obj); 1399 } else { //序列化实现java.io.Serializable类对象属性的值 1400 writeSerialData(obj, desc); 1401 } -下略- ---------------------------------------------------------------------------------------------------- 由上述代码的1397行可知,当序列化类是Externalizable类型时将执行 writeExternalData方法,当是Serializable类型的是否才执行writeSerialData方法,而在writeExternalData方法中,序列化类会调用自定义的writeExternal方法执行自定义的序列化操作。 新建一个自己的序列化类 自定义一个自己的序列化类需要三个步骤,第一步需要继承ObjectOutputStream(ObjectInputStream);第二步在构造函数调用父类的无参构造函数;第三步重写父类的writeObjectOverride(readObjcetOverride)方法,在该方法中自定义序列化方案。 ---------------------------------------------------------------------------------------------------- public class CustomObjectOutputStream extends ObjectOutputStream{ private OutputStream objOut; public CustomObjectOutputStream(OutputStream out) throws IOException { super(); this.objOut = out; } @Override protected void writeObjectOverride(Object obj) throws IOException { //自定义序列化方案 } } ---------------------------------------------------------------------------------------------------- 以上是自定义的序列化类的实现方式;反序列化类与此类似,这里不再赘述。这样做的原理是什么?我们一看JDK源码就清楚了。 java.io ├java.io.ObjectOutputStream() └java.io.ObjectOutputStream.writeObject() ---------------------------------------------------------------------------------------------------- 254 protected ObjectOutputStream() throws IOException, SecurityException { -中略- //分配私有数据成员为空,这样便于自定义自己的序列化类 259 bout = null; 260 handles = null; 261 subs = null; //自定义方法的调用的关键变量 262 enableOverride = true; 263 debugInfoStack = null; 264 } -中略- 324 public final void writeObject(Object obj) throws IOException { 325 if (enableOverride) { 326 writeObjectOverride(obj); 327 return; 328 } -下略- ---------------------------------------------------------------------------------------------------- 在自定义序列化类的构造函数中调用父类的无参构造函数,保证了enableOverride变量为true。那么当执行writeObject序列化类时,会就会调用自定义序列化方法writeObjectOverride。 序列化流验证机制 一般情况下,我们认为序列化流中的数据总是与最初写到流中的数据一致,这并没有问题。但当黑客获取流信息并篡改一些敏感信息重新序列化到流中后,用户通过反序列化得到的将是被篡改的信息。Java序列化提供一套验证机制。序列化类通过实现 java.io.ObjectInputValidation接口,就可以做到验证了。改写后的Apple类如下: ---------------------------------------------------------------------------------------------------- package com.fnst.infoQ; public class Apple extends Fruit implements Serializable,ObjectInputValidation { private String name; private transient int price; public Apple(String _name,int _price){ super(); this.name = _name; this.price = _price; } public Apple() {this.name = "Default Apple"; price = 4;} public void validateObject() throws InvalidObjectException { // TODO Auto-generated method stub //添加验证的对象属性的hash值,来判断序列化流是否被篡改。 boolean flag = hash(); if(flag){ //未被篡改 }else{ throw new InvalidObjectException("流信息被篡改了"); } } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); //将该序列化对象注册到序列化流对象上 in.registerValidation(this, 0); } } ---------------------------------------------------------------------------------------------------- Java序列化验证机制的基本原理:将想要验证的序列化类注册到 java.io.ObjectInputStream类的验证回调列表中,将对象从流中反序列化出来后,会遍历回调列表,调用序列化类的validateObject方法来进行验证操作。 java.io.ObjectInputStream.readObject代码如下: ---------------------------------------------------------------------------------------------------- 340 public final Object readObject() 341 throws IOException, ClassNotFoundException 342 { -中略- //从流中反序列化出对象obj 350 Object obj = readObject0(false); 351 handles.markDependency(outerHandle, passHandle); //查看反序列化出来的类是否存在,如果不存在则对象缓冲器中将存在、、//ClassNotFoundException 异常对象 352 ClassNotFoundException ex = handles.lookupException(passHandle); 353 if (ex != null) { 354 throw ex; 355 } 356 if (depth == 0) { //遍历回调列表,并调用各个注册对象的validateObject方法 357 vlist.doCallbacks(); 358 } 359 return obj; -下略- ---------------------------------------------------------------------------------------------------- 在Apple.readObject方法中in.registerValidation(this, 0);的调用就是将本类对象(this)注册到验证回调列表(vlist)中。Vlist对象的doCallbacks方法如下: java.io.ObjectOutputStream.readObject() └java.io.ObjectOutputStream.ValidationList.doCallbacks () ---------------------------------------------------------------------------------------------------- 2199 void doCallbacks() throws InvalidObjectException { -中略- //遍历验证回调列表 2201 while (list != null) { //java安全特权代码区域,具体的含义我们将在下一期的java安全管理中介绍 2202 AccessController.doPrivileged( 2203 new PrivilegedExceptionAction() 2204 { 2205 public Object run() throws InvalidObjectException { //调用各个注册对象的validateObject方法 2206 list.obj.validateObject(); 2207 return null; 2208 } 2209 }, list.acc); 2210 list = list.next; 2211 } ---------------------------------------------------------------------------------------------------- 小结 本文从JDK源码的角度分析了java序列化提供的安全机制,包括自定义序列化机制以及序列化流的验证机制;其中自定义序列化提供了三种方案: java.io.Serializable、 java.io.Externalizable和自定义序列化类。第一种主要优点是java提供默认的内建支持,并且易于实现,缺点是占用空间过大,速度较慢;第二种主要优点就是系统开销较少,速度较快,但是实现需要程序员来完成;第三种,存在很大的灵活性,程序员完全可以发挥自己的聪明才智编写出更好的序列化方案。除了安全性支持外,java序列化还提供了序列化类的重构性,例如当类中增加了一项属性域的时候,当序列化化类的父类版本发生变化的时候,是否还能兼容以前的序列化数据?这些我们将在下一篇<<java序列化的可重构性>>中详细讨论。 -以上- 2013年09月22日于南京 。 |
|