Java原生序列化与反序列化代码简要分析

前言

写这篇文章目的主要在于进一步理解何为java原生反序列化,并且回答的几个问题。

  1. 为什么就java反序列化使用而言是反序列化类的readObject开始?
  2. 为什么resolveClass方法可以防御反序列化?
  3. 为什么在反序列化数据后面插入脏数据会不会影响反序列化?

ps: 碍于水平有限只会在代码表层做宏观和中观的分析,并不会深入特别底层。

测试代码

序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.*;

class User implements Serializable {
int age = 20;
int money = 100;
String firstname = "cl0und";
String lastname = "lisan";

User(){
System.out.println("无参数构造方法");
}
}

public class Serial {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oops = new ObjectOutputStream(new FileOutputStream("./serializable.txt"));
oops.writeObject(new User());
}
}

反序列化

1
2
3
4
5
6
7
8
9
10
11
12
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;


public class Unserial {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream oips = new ObjectInputStream(new FileInputStream("./serializable.txt"));
User user = (User) oips.readObject();
System.out.println(user.age);
}
}

序列化

预处理

预处理的意义是拿到类的描述符类ObjectStreamClass,后续对类的内省和写入序列化数据都由他完成或调度。
image.png

image.png

image.png

序列化数据

可以看到一共有三步,首先是TC_OBJECT,其次是类的元信息,最后是类的具体数据。
image.png

第一步就写个标志头,比较简单我们下面直接从第二步开始看。

writeClassDesc

在序列化对象的数据之前,首先会有一个对象类型的分类判断,这里分成四种类型来处理即null类型(TC_NULL)、handle类型(TC_REFERENCE)、代理类型(TC_PROXYCLASSDESC)、普通类型(TC_CLASSDESC)。我们这里的User是普通的类所以走最后的writeNonProxyDesc。
image.png

writeNonProxyDesc会先调用writeClassDescriptor把自己的属性信息写入,然后递归调用writeClassDesc写入父类元信息
image.png

细看一下writeClassDescriptor,这里面会调用writeNonProxy。
image.png

最后调用writeNonProxy,在for循环里面写入属性信息。
image.png

这一段的写入如果最后用dump出来,就是下面这个样子。
image.png

writeSerialData

把类属性信息写去之后,接下来就是写类属性对应的具体值了。image.png

image.png

ps:非原生数据类型指的是字符串、数组、枚举类型、对象。
_
一个完整的dump如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 4 - 0x00 04
Value - User - 0x55736572
serialVersionUID - 0xbb 67 4f fb 9b a9 d6 bb
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 4 - 0x00 04
Fields
0:
Int - I - 0x49
fieldName
Length - 3 - 0x00 03
Value - age - 0x616765
1:
Int - I - 0x49
fieldName
Length - 5 - 0x00 05
Value - money - 0x6d6f6e6579
2:
Object - L - 0x4c
fieldName
Length - 9 - 0x00 09
Value - firstname - 0x66697273746e616d65
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 01
Length - 18 - 0x00 12
Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
3:
Object - L - 0x4c
fieldName
Length - 8 - 0x00 08
Value - lastname - 0x6c6173746e616d65
className1
TC_REFERENCE - 0x71
Handle - 8257537 - 0x00 7e 00 01
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 02
classdata
User
values
age
(int)20 - 0x00 00 00 14
money
(int)100 - 0x00 00 00 64
firstname
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 03
Length - 6 - 0x00 06
Value - cl0und - 0x636c30756e64
lastname
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 04
Length - 5 - 0x00 05
Value - lisan - 0x6c6973616e

可以看到在序列化的时候世界上是写入了各字段长度的,所以在后面反序列化读的时候是按照字段长度来进行读取的。这也解释了为什么在反序列化数据后面插入脏数据会不会影响反序列化

反序列化

预处理

反序列化和序列化互为称操作,其主要的操作在readObject0里面
image.png

case到object进行反序列化,整个object的反序化在readOrdinaryObject中
image.png

和序列化对称还是分两步,第一步是读取出类的元信息(readClassDesc),第二步是读取出对象属性具体的数据(readSerialData)。
image.png

反序列化数据

readClassDesc

在序列化那里提到过,对把类类型分成四个类型处理,这里的User是非代理普通类,就走readNonProxyDesc。
image.png

readNonProxyDesc中主要的操作在readClassDescriptor、resolveClass、initNonProxy(注意这里initNonProxy的最后一个传参,这里递归调用了readClassDesc)。
image.png

readClassDescriptor读出了类的元数据并把每一个属性名抽象成了ObjectStreamField类
image.png

resolveClass对类类型进行了构建,注意这里第二参数为false,意味着并不会执行类的初始化(static代码块不执行)

image.png

另外注意这里解释了为什么设置反序列化防御的点是在resolveClass,如果想对反序列化设置防御,就需要自己实现一个ObjectInputStream。

1
2
3
4
5
6
7
8
9
10
class MyObjInputStream extends ObjInputStream {
public MyObjInputStream(InputStream in) throws IOException{
super(in);
}

protected Class<?> resolveClass(ObjInputStream desc) throws IOException, ClassNotFoundException {
System.out.println("defence");
return super.resolveClass(desc)
}
}

initNonProxy又构建了类的元信息。
image.png

最后desc(ObjectStreamClass)传出给外层readOrdinaryObject。首先会进行对象的初始化,然后读出数据注入类中。
image.png

readSerialData

如果我们自定义了readObject方法,那么这里就会invoke,这里解释了为什么就java反序列化使用而言是反序列化类的readObject开始。
image.png

但是在User类中在我们并没有自定义反序列化方法随意还是走默认路线。
image.png

defaultReadFields,中第一个循环把原生类型数据赋给obj,第二个循环把数组、枚举类型、对象类型赋给obj。
image.png

补充一张整个反序列化的时序图。
image.png

从SerialKiller来java反序列化防御

https://github.com/ikkisoft/SerialKiller
原生使用

1
2
ObjectInputStream ois = new ObjectInputStream(is);
String msg = (String) ois.readObject();

加上SerialKiller后的使用

1
2
ObjectInputStream ois = new SerialKiller(is, "/etc/serialkiller.conf");
String msg = (String) ois.readObject();

可以看到它的原理就是上面提的自己实现流并且在resolveClass处加上白名单或者黑名单的过滤。
image.png

当父子接口同时实现了Serializable接口时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class UserBase implements Serializable{
public int age;
UserBase(){
System.out.println("UserBase无参构造");
}

UserBase(int age){
this.age = age;
System.out.println("UserBase有参构造");
}
}

class User2 extends UserBase implements Serializable{

User2(){
System.out.println("User2无参构造");
}

User2(int age){
this.age = age;
System.out.println("User2有参构造");
}
}

控制台输出

1
2
3
4
UserBase无参构造
User2有参构造
-----------------
2333

当只有子类实现Serializable接口父类没有实现时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.io.*;

public class SerialAndUnserial {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oops = new ObjectOutputStream(new FileOutputStream("./serializable.txt"));
oops.writeObject(new User2(2333));

System.out.println("-----------------");
ObjectInputStream oips = new ObjectInputStream(new FileInputStream("./serializable.txt"));
User2 user = (User2) oips.readObject();

System.out.println(user.age);
}
}


class UserBase {
public int age;
UserBase(){
System.out.println("UserBase无参构造");
}

UserBase(int age){
this.age = age;
System.out.println("UserBase有参构造");
}
}

class User2 extends UserBase implements Serializable{

User2(){
System.out.println("User2无参构造");
}

User2(int age){
this.age = age;
System.out.println("User2有参构造");
}
}
1
2
3
4
5
UserBase无参构造
User2有参构造
-----------------
UserBase无参构造
0

总结及一些tip

  • 序列化时,当写入类的元数据的时候,是先写子类的类元数据,然后递归调用的写入父类的类元数据(只有实现序列化接口的才会有类元数据)。
  • 防御反序列化的原理是在resolveClass处设防而不是readResolve
  • 在序列化数据末尾加入脏数据不会影响正常的反序列化。

参考

java中的序列化与反序列化及其源码分析(特别详细)
先知大会议题Java反序列化实战
[从WebLogic看反序列化漏洞的利用与防御](

Author

李三(cl0und)

Posted on

2020-02-14

Updated on

2020-07-11

Licensed under