您的位置 首页 java

Java小白注意了!学好java反序列化,吐血总结

Java 反序列化是java安全的基础,想要学好java反序列化,就不能只看看相关文章,要自己动手实践,看看java反序列化到底是怎么回事。今天小华跟大家分享“java序列化和反序列化的那些事”。

首先,我们先了解下,什么是java序列化?

Java Serialization(序列化):将java对象以一连串的字节保存在磁盘文件中的过程,也可以说是保存java对象状态的过程。序列化可以将数据永久保存在磁盘上(通常保存在文件中)。

下面我们就手敲代码,自己实现一个序列化程序!

 public class main {
    private  static  class innerClass implements Serializable {
         String  name;
        String test;
        int years;


        public innerClass(){}
        public innerClass(String name, String test, int years) {
            this.name = name;
            this.test = test;
            this.years = years;
        }


        @Override
        public String toString() {
            return "innerClass{" +
                    "name='" + name + ''' +
                    ", test='" + test + ''' +
                    ", years=" + years +
                    '}';
        }
    }
    public static  void  main(String[] args) throws  Exception  {
        innerClass ic = new innerClass();//创建对象
        ic.name="123";
        ic.test="test";
        ic.years=123546;
        File f = new File("java_security/1.txt");// 模块名/文件名
        if(f. exists ()) {
            System.out.pr Int ln("文件存在");
        }else{
            //否则创建新文件
            f.createNewFile();
        }
        try{
             FileOutputStream  fos=new FileOutputStream(f);
            ObjectOutputStream oos=new ObjectOutputStream(fos);
            oos.writeObject(ic);//将ic对象序列化写入文件
            oos.flush();
            oos.close();
            fos.close();
        }catch (Exception e) {
            System.out.println(e);
        }
    }
}  

注意点:

 1、序列化对象需要实现 Serializable接口 
2、序列化需要使用ObjectOutputStream对象创建对象输出流
3、ObjectOutputStream对象序列化所用方法writeObject()
4、ObjectOutputStream对象需要文件输出流作为输出目标
5、FileOutputStream对象需要一个文件对象  

因此,我们整个实现过程为:创建需要序列化的对象、创建文件对象、创建文件输出流对象、创建对象输出流对象、序列化。

运行程序,我们得到1.txt。

Java小白注意了!学好java反序列化,吐血总结

可以看到,在java_security模块下生成了1.txt文件,里面包含着innerClass对象(即我刚刚序列化的对象)的序列化字节码。

这些 字节码 都是我们人为不可看的,很不利于我们在对于java反序列化或者java安全方面的研究,有什么办法能解决这个问题呢?

Serialization Dumper

我们可以使用SerializationDumper来将序列化字节码转化为方便阅读的形式,下面我们就一起来装一下SerializationDumper吧。

  git  clone   

进入安装路径,执行build.bat 文件。

 E:web-ToolsSerializationDumper> build.bat  

然后就可以在该目录中使用SerializationDumper.jar了,接下来我们就试试使用SerializationDumper。

将SerializationDumper拖入项目。

 E: IntelliJ  IDEA 2018.2.7projectjava_security>java -jar SerializationDumper.jar
Usage:
        SerializationDumper <hex-ascii-data>
        SerializationDumper -f <file-containing-hex-ascii>
        SerializationDumper -r <file-containing-raw-data>


Rebuild a dumped stream:
        SerializationDumper -b <input-file> <output-file>  

按照上述使用方法 使用 -r 处理raw-data文件。

 E:IntelliJ IDEA 2018.2.7projectjava_security>java -jar SerializationDumper.jar -r 1.txt > 2.txt


STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 15 - 0x00 0f
        Value - main$innerClass - 0x6d61696e24696e6e6572436c617373
      serialVersionUID - 0xca 3e 75 e0 69 b7 50 c5
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_ Serializable 
      fieldCount - 3 - 0x00 03
      Fields
        0:
          Int - I - 0x49
          fieldName
            Length - 5 - 0x00 05
            Value - years - 0x7965617273
        1:
          Object - L - 0x4c
          fieldName
            Length - 4 - 0x00 04
            Value - name - 0x6e616d65
          className1
            TC_STRING - 0x74
              newHandle 0x00 7e 00 01
              Length - 18 - 0x00 12
              Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
        2:
          Object - L - 0x4c
          fieldName
            Length - 4 - 0x00 04
            Value - test - 0x74657374
          className1
            TC_REFERENCE - 0x71
              Handle - 8257537 - 0x00 7e 00 01
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 02
    classdata
      main$innerClass
        values
          years
            (int)123546 - 0x00 01 e2 9a
          name
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 03
                Length - 3 - 0x00 03
                Value - 123 - 0x313233
          test
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 04
                Length - 4 - 0x00 04
                Value - test - 0x74657374  
Java小白注意了!学好java反序列化,吐血总结

这里就可以很清楚的看到,跟我们之前设定的属性是相符合的。

反序列化

 try{
     FileInputStream  fis=new FileInputStream("java_security/1.txt");
    ObjectInputStream ois = new Object InputStream (fis);
    innerClass ic2=(innerClass)ois.readObject();
    System.out.println(ic2);
    ois.close();
    fis.close();
}catch(Exception e) {
    System.out.println(e);
}  

与序列化的代码片相反,反序列化将文件内的字节流重新反序列化为对象。反序列化流程如上,便不再赘述。

Java小白注意了!学好java反序列化,吐血总结

###

复写readObject和writeObject

经过上面简单的案例,大家应该能了解到序列化与反序列化的大体步骤,接下来就开始了解readObject和writeObject的复写。

我们看到类实现的Serializable 接口,它是没有任何内容的,相当于一个标识符。

Java小白注意了!学好java反序列化,吐血总结

那么我们该怎么复写readObject和writeObject呢。分析源码:

 public final void writeObject(Object obj) throws IOException {
    if (enable Override ) {
        writeObjectOverride(obj);
        return;
    }
    try {
        writeObject0(obj, false);
    } catch (IOException ex) {
        if (depth == 0) {
            writeFatalException(ex);
        }
        throw ex;
    }
}  

首先从writeObject方法进了writeObject0。

 if (obj  instanceof  String) {
                writeString((String) obj, unshared);
            } else if (cl.isArray()) {
                writeArray(obj, desc, unshared);
            } else if (obj instanceof Enum) {
                write Enum ((Enum<?>) obj, desc, unshared);
            } 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());
                }
            }  
Java小白注意了!学好java反序列化,吐血总结

跟踪语句,我们找到了这样一句,若obj或其子类实现了Serializable,则进入这个判断语句,即进入writeOrdinaryObject方法。

instanceof 是java的保留关键字。他的作用就是测试左边的对象是不是右边类的实例,是的话就返回true,不是的话返回false。

 private void writeOrdinaryObject(Object obj,
                                 ObjectStreamClass desc,
                                  boolean  unshared)
{
    ......
        if (desc.isExternalizable() && !desc.isProxy()) {
            writeExternalData((Externalizable) obj);
        } else {
            writeSerialData(obj, desc);
        }
     ......
}  

这里按按实现了Externalizable接口或Serializable接口分别执行writeExternalData和writeSerialData方法,我们这里进入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;
            if (slotDesc.hasWriteObjectMethod()) {
                ...
            }
        }
    ......
}  

进入hasWriteObjectMethod方法。

 boolean hasWriteObjectMethod() {
    requireInitialized();
    return (writeObjectMethod != null);
}  

康康writeObjectMethod变量。

 /** class-defined writeObject method, or null if none */private Method writeObjectMethod;  

这里可以看到了,如果类中定义了 writeObject 。查找该变量康康。

Java小白注意了!学好java反序列化,吐血总结

好!这里可以看到,利用了java反射得到对象的writeObject方法,这里就说判断序列化对象中是否含有writeObject方法。

回到writeSerialData方法。

 if (slotDesc.hasWriteObjectMethod()) {//如果目标类重写了writeObject方法
    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);
        //利用反射执行类中的writeObject方法
        slotDesc.invokeWriteObject(obj, this);
        bout.setBlockDataMode(false);
        bout.writeByte(TC_ENDBLOCKDATA);
    } finally {
        curContext.setUsed();
        curContext = oldContext;
        if (extendedDebugInfo) {
            debugInfoStack.pop();
        }
    }


    curPut = oldPut;
} else {
    //没重新,执行默认反序列化方法
    defaultWriteFields(obj, slotDesc);
}  

看到这里,我们基本上就可以得出结论了:

要重写readObject和writeObject方法,只需要在需要序列化和反序列化中的类中写相应的方法。简单的说,以readObject方法为例,在ObjectInputStream对象调用readObject时,经过一系列调用,检测你需要序列化对象中是否含有readObject,如果有则通过java反射特性,得到需要序列化对象的readObject方法,否则使用默认的readObject方法!

接下来,我们来改写一下inner类,加入readObject和writeObject方法。

 private static class innerClass implements Serializable {
    String name;
    String test;
    int years;


    public innerClass(){}
    public innerClass(String name, String test, int years) {
        this.name = name;
        this.test = test;
        this.years = years;
    }


    @Override
    public String toString() {
        return "innerClass{" +
                "name='" + name + ''' +
                ", test='" + test + ''' +
                ", years=" + years +
                '}';
    }
    private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException { // 自定义反序列化实现


        System.out.println("readObject execute");
        is.defaultReadObject();
        String message = (String) is.readObject(); System.out.println(message);
    }


    private void writeObject(ObjectOutputStream is) throws IOException, ClassNotFoundException { // 自定义序列化实现
        System.out.println("writebject execute");
        is.defaultWriteObject();
        is.writeObject("This is a object");
    }
}  

Java小白注意了!学好java反序列化,吐血总结

运行结果无疑是成功了。

再康康序列化出来的结构。

 STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 15 - 0x00 0f
        Value - main$innerClass - 0x6d61696e24696e6e6572436c617373
      serialVersionUID - 0xca 3e 75 e0 69 b7 50 c5
      newHandle 0x00 7e 00 00
      classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
      fieldCount - 3 - 0x00 03
      Fields
        0:
          Int - I - 0x49
          fieldName
            Length - 5 - 0x00 05
            Value - years - 0x7965617273
        1:
          Object - L - 0x4c
          fieldName
            Length - 4 - 0x00 04
            Value - name - 0x6e616d65
          className1
            TC_STRING - 0x74
              newHandle 0x00 7e 00 01
              Length - 18 - 0x00 12
              Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
        2:
          Object - L - 0x4c
          fieldName
            Length - 4 - 0x00 04
            Value - test - 0x74657374
          className1
            TC_REFERENCE - 0x71
              Handle - 8257537 - 0x00 7e 00 01
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 02
    classdata
      main$innerClass
        values
          years
            (int)123546 - 0x00 01 e2 9a
          name
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 03
                Length - 3 - 0x00 03
                Value - 123 - 0x313233
          test
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 04
                Length - 4 - 0x00 04
                Value - test - 0x74657374
        objectAnnotation
          TC_STRING - 0x74
            newHandle 0x00 7e 00 05
            Length - 16 - 0x00 10
            Value - This is a object - 0x546869732069732061206f626a656374
          TC_ENDBLOCKDATA - 0x78  

最后多出来一节。

 objectAnnotation
  TC_STRING - 0x74
    newHandle 0x00 7e 00 05
    Length - 16 - 0x00 10
    Value - This is a object - 0x546869732069732061206f626a656374
  TC_ENDBLOCKDATA - 0x78  

这就意味着我们可以在序列化时向字节码中写入一些这个对象的属性以外的东西。这个特性就让Java的开发变得非常灵活。

文章来源:智云一二三科技

文章标题:Java小白注意了!学好java反序列化,吐血总结

文章地址:https://www.zhihuclub.com/189580.shtml

关于作者: 智云科技

热门文章

网站地图