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=""; | |
ic.test="test"; | |
ic.years=; | |
File f = new File("java_security/.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); | |
} | |
} | |
} |
注意点:
、序列化对象需要实现 Serializable接口 | |
、序列化需要使用ObjectOutputStream对象创建对象输出流 | |
、ObjectOutputStream对象序列化所用方法writeObject() | |
、ObjectOutputStream对象需要文件输出流作为输出目标 | |
、FileOutputStream对象需要一个文件对象 |
因此,我们整个实现过程为:创建需要序列化的对象、创建文件对象、创建文件输出流对象、创建对象输出流对象、序列化。
运行程序,我们得到1.txt。
可以看到,在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.2.7projectjava_security>java -jar SerializationDumper.jar -r 1.txt > 2.txt | |
STREAM_MAGIC -xac ed | |
STREAM_VERSION -x00 05 | |
Contents | |
TC_OBJECT -x73 | |
TC_CLASSDESC -x72 | |
className | |
Length - - 0x00 0f | |
Value - main$innerClass -x6d61696e24696e6e6572436c617373 | |
serialVersionUID -xca 3e 75 e0 69 b7 50 c5 | |
newHandlex00 7e 00 00 | |
classDescFlags -x02 - SC_ Serializable | |
fieldCount - - 0x00 03 | |
Fields: | |
Int - I -x49 | |
fieldName | |
Length - - 0x00 05 | |
Value - years -x7965617273 | |
: | |
Object - L -x4c | |
fieldName | |
Length - - 0x00 04 | |
Value - name -x6e616d65 | |
className | |
TC_STRING -x74 | |
newHandlex00 7e 00 01 | |
Length - - 0x00 12 | |
Value - Ljava/lang/String; -x4c6a6176612f6c616e672f537472696e673b | |
: | |
Object - L -x4c | |
fieldName | |
Length - - 0x00 04 | |
Value - test -x74657374 | |
className | |
TC_REFERENCE -x71 | |
Handle - - 0x00 7e 00 01 | |
classAnnotations | |
TC_ENDBLOCKDATA -x78 | |
superClassDesc | |
TC_NULL -x70 | |
newHandlex00 7e 00 02 | |
classdata | |
main$innerClass | |
values | |
years | |
(int) - 0x00 01 e2 9a | |
name | |
(object) | |
TC_STRING -x74 | |
newHandlex00 7e 00 03 | |
Length - - 0x00 03 | |
Value - - 0x313233 | |
test | |
(object) | |
TC_STRING -x74 | |
newHandlex00 7e 00 04 | |
Length - - 0x00 04 | |
Value - test -x74657374 |
这里就可以很清楚的看到,跟我们之前设定的属性是相符合的。
反序列化
try{ | |
FileInputStream fis=new FileInputStream("java_security/1.txt"); | |
ObjectInputStream ois = new Object InputStream (fis); | |
innerClass ic=(innerClass)ois.readObject(); | |
System.out.println(ic); | |
ois.close(); | |
fis.close(); | |
}catch(Exception e) { | |
System.out.println(e); | |
} |
与序列化的代码片相反,反序列化将文件内的字节流重新反序列化为对象。反序列化流程如上,便不再赘述。
###
复写readObject和writeObject
经过上面简单的案例,大家应该能了解到序列化与反序列化的大体步骤,接下来就开始了解readObject和writeObject的复写。
我们看到类实现的Serializable 接口,它是没有任何内容的,相当于一个标识符。
那么我们该怎么复写readObject和writeObject呢。分析源码:
public final void writeObject(Object obj) throws IOException { | |
if (enable Override ) { | |
writeObjectOverride(obj); | |
return; | |
} | |
try { | |
writeObject(obj, false); | |
} catch (IOException ex) { | |
if (depth ==) { | |
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()); | |
} | |
} |
跟踪语句,我们找到了这样一句,若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 =; 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反射得到对象的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"); | |
} | |
} |
运行结果无疑是成功了。
再康康序列化出来的结构。
STREAM_MAGIC -xac ed | |
STREAM_VERSION -x00 05 | |
Contents | |
TC_OBJECT -x73 | |
TC_CLASSDESC -x72 | |
className | |
Length - - 0x00 0f | |
Value - main$innerClass -x6d61696e24696e6e6572436c617373 | |
serialVersionUID -xca 3e 75 e0 69 b7 50 c5 | |
newHandlex00 7e 00 00 | |
classDescFlags -x03 - SC_WRITE_METHOD | SC_SERIALIZABLE | |
fieldCount - - 0x00 03 | |
Fields: | |
Int - I -x49 | |
fieldName | |
Length - - 0x00 05 | |
Value - years -x7965617273 | |
: | |
Object - L -x4c | |
fieldName | |
Length - - 0x00 04 | |
Value - name -x6e616d65 | |
className | |
TC_STRING -x74 | |
newHandlex00 7e 00 01 | |
Length - - 0x00 12 | |
Value - Ljava/lang/String; -x4c6a6176612f6c616e672f537472696e673b | |
: | |
Object - L -x4c | |
fieldName | |
Length - - 0x00 04 | |
Value - test -x74657374 | |
className | |
TC_REFERENCE -x71 | |
Handle - - 0x00 7e 00 01 | |
classAnnotations | |
TC_ENDBLOCKDATA -x78 | |
superClassDesc | |
TC_NULL -x70 | |
newHandlex00 7e 00 02 | |
classdata | |
main$innerClass | |
values | |
years | |
(int) - 0x00 01 e2 9a | |
name | |
(object) | |
TC_STRING -x74 | |
newHandlex00 7e 00 03 | |
Length - - 0x00 03 | |
Value - - 0x313233 | |
test | |
(object) | |
TC_STRING -x74 | |
newHandlex00 7e 00 04 | |
Length - - 0x00 04 | |
Value - test -x74657374 | |
objectAnnotation | |
TC_STRING -x74 | |
newHandlex00 7e 00 05 | |
Length - - 0x00 10 | |
Value - This is a object -x546869732069732061206f626a656374 | |
TC_ENDBLOCKDATA -x78 |
最后多出来一节。
objectAnnotation | |
TC_STRING -x74 | |
newHandlex00 7e 00 05 | |
Length - - 0x00 10 | |
Value - This is a object -x546869732069732061206f626a656374 | |
TC_ENDBLOCKDATA -x78 |
这就意味着我们可以在序列化时向字节码中写入一些这个对象的属性以外的东西。这个特性就让Java的开发变得非常灵活。