参考文献 https://github.com/Y4er/dotnet-deserialization/ 一代人一代人传下来的文献。学。
01-环境搭建 搭建牢记于心,一开始以为很简单,但是对刚入手一门语言的人菜鸡还是太勉强了,花了也有2小时去研究一下包管理、lib添加以及一系列的兼容问题 all in one,就不写了。 笔者使用的编译器是Rider,被JB全家桶磨平了,用JB只有一次和无数次。 个人感觉挺不错的,体验感不错。比Visual studio美观(
02-基础序列化与反序列化 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 using System;using System.IO;using System.Runtime.Serialization;using System.Runtime.Serialization.Formatters.Binary;namespace DonNET_Deserialization ;public class NormalSerialize { [Serializable ] public class Myclass { public int n1; [NonSerialized ] public int n2; public String str; } class Program { public static void BinaryFormatterSerialize (string file, object o ) { BinaryFormatter binaryFormatter = new BinaryFormatter(); FileStream fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None); binaryFormatter.Serialize(fileStream, o); fileStream.Close(); Console.WriteLine($"serialize object {o} to file {file} ." ); } public static object BinaryFormatterDeserialFromFile (string file ) { IFormatter formatter = new BinaryFormatter(); Stream stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); object o = formatter.Deserialize(stream); stream.Close(); return o; } static void Main (string [] args ) { try { Myclass myObject = new Myclass(); myObject.n1 = 1 ; myObject.n2 = 2 ; myObject.str = "Boogipop" ; BinaryFormatterSerialize("1.bin" , myObject); Myclass myObject1 = (Myclass)BinaryFormatterDeserialFromFile("1.bin" ); Console.WriteLine($"n1:{myObject1.n1} " ); Console.WriteLine($"NonSerialized n2:{myObject1.n2} " ); Console.WriteLine($"str:{myObject1.str} " ); } catch (Exception e) { Console.WriteLine(e.Message); } } } }
运行结果如上,这里我们需要加上Serializable
注解,并且可以发现添加了NonSerialized
注解的字段并没有参与序列化,因此反序列化输出时它的值为0 它的序列化文件格式如上
Formatter就是你需要进行什么样子的序列化和反序列化,在DOTNET中,Formatter大概分为以下几类
BinaryFormatter 二进制流序列化
SoapFormatter soap格式的序列化
LosFormatter 一般用于序列化存储视图流状态
ObjectStateFormatter 一般用于序列化和反序列化状态对象图
XmlSerializer
JsonSerializer
等等其他……..
其中XmlSerializer和JsonSerializer咱们在不久后的将来还会见到的XD。本文主要讲一些基础内容知识。 其中Formatters都像BinaryFormatter一样实现了2个类IFormatter、IRemotingFormatter
其中IRemotiongFormatter主要提供RPC服务,它也实现IFormatter接口 因此我们只需要关注IFormatter即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 using System.IO;using System.Runtime.InteropServices;#nullable disable namespace System.Runtime.Serialization { [ComVisible(true) ] public interface IFormatter { object Deserialize (Stream serializationStream ) ; void Serialize (Stream serializationStream, object graph ) ; ISurrogateSelector SurrogateSelector { get ; set ; } SerializationBinder Binder { get ; set ; } StreamingContext Context { get ; set ; } } }
IFormatter类的定义如上,他有三个属性
ISurrogateSelector
SerializationBinder
StreamingContext
这里引用Y4er师傅的介绍。
类 字段名
含义用途
ISurrogateSelector SurrogateSelector
序列化代理选择器 接管formatter的序列化或反序列化处理
SerializationBinder Binder
用于控制在序列化和反序列化期间使用的实际类型
StreamingContext Context
序列化流上下文 其中states字段包含了序列化的来源和目的地
其中我们需要重点注意的就是ISurrogateSelector,代理选择器。
04-序列化和反序列化生命周期 这里还是以BinaryFormatter为例子,介绍一下DOTNET反序列化和序列化的生命周期。上面介绍了IFormatter的三个属性,而这三个属性具体含义和例子是这样的
1.首先确定formatter是否有代理选择器,如果有则检查代理选择器要处理的对象类型是否和给定的对象类型一致,如果一致,代理选择器会调用ISerializable.GetObjectData() 2.如果没有代理选择器,或者代理选择器不处理该对象类型,则检查对象是否有[Serializable]特性。如果不能序列化则抛出异常 3.检查该对象是否实现ISerializable接口,如果实现就调用其GetObjectData方法 4.如果没实现ISerializable接口就使用默认的序列化策略,序列化所以没标记[NonSerialized]的字段
用流程图表示就是下面这样 并且序列化和反序列化有以下四个回调事件
04.1-代理器模式 和魔术方法一样,他们是以注解的形式写在方法上的。现在准备以下代码做测试
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 using System;using System.IO;using System.Runtime.Serialization;using System.Runtime.Serialization.Formatters.Binary;using System.Security.Permissions;namespace NormalSerialize { [Serializable ] public class MyObject : ISerializable { public string str { get ; set ; } public MyObject () { } protected MyObject (SerializationInfo info, StreamingContext context ) { Console.WriteLine("MyObject(SerializationInfo info, StreamingContext context)" ); str = info.GetString("str" ); } [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter) ] public virtual void GetObjectData (SerializationInfo info, StreamingContext context ) { Console.WriteLine("GetObjectData of MyObject.class" ); info.AddValue("str" , str, typeof (string )); } [OnDeserializing ] private void TestOnDeserializing (StreamingContext sc ) { Console.WriteLine("TestOnDeserializing" ); } [OnDeserialized ] private void TestOnDeserialized (StreamingContext sc ) { Console.WriteLine("TestOnDeserialized" ); } [OnSerializing ] private void TestOnSerializing (StreamingContext sc ) { Console.WriteLine("TestOnSerializing" ); } [OnSerialized ] private void TestOnSerialized (StreamingContext sc ) { Console.WriteLine("TestOnSerialized" ); } } class MySerializationSurrogate : ISerializationSurrogate { public void GetObjectData (object obj, SerializationInfo info, StreamingContext context ) { Console.WriteLine("GetObjectData of ISerializationSurrogate" ); info.AddValue("str" , ((MyObject)obj).str); } public object SetObjectData (object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector ) { Console.WriteLine("SetObjectData of ISerializationSurrogate" ); MyObject m = new MyObject(); m.str = (string )info.GetValue("str" , typeof (string )); return m; } } class Program { static void Main (string [] args ) { try { MyObject myObject = new MyObject(); myObject.str = "hello" ; using (MemoryStream memoryStream = new MemoryStream()) { BinaryFormatter binaryFormatter = new BinaryFormatter(); SurrogateSelector ss = new SurrogateSelector(); ss.AddSurrogate(typeof (MyObject), binaryFormatter.Context, new MySerializationSurrogate()); binaryFormatter.SurrogateSelector = ss; binaryFormatter.Serialize(memoryStream, myObject); memoryStream.Position = 0 ; myObject = null ; myObject = (MyObject)binaryFormatter.Deserialize(memoryStream); Console.WriteLine(myObject.str); } } catch (Exception e) { Console.WriteLine(e.StackTrace); } } } }
可以看到这里首先准备了一个MyObject类,实现了ISerializable
接口,并且准备了一个代理器MySerializationSurrogate
,并且在序列化的时候指定了代理器,因此我们的流程应该是
OnDeserializing方法(序列化开始)
代理器的GetObjectData,获取属性
OnSerialized方法(序列化结束)
OnDeserializing(反序列化开始)
代理器的SetObjectData,设置属性
OnDeserialized(反序列化结束)
04.2-非代理器&&ISerializable模式 这一次我们删除代理器设置。将binaryFormatter.SurrogateSelector = ss;
注释 结果如上,顺序变成了
OnSerializing(序列化开始)
由于实现了ISerializable接口,因此会调用本身的GetObjectData方法
OnSerialized(序列化结束)
OnDeserializing(反序列化开始)
类的实例化。protected MyObject(SerializationInfo info, StreamingContext context)
,在反序列化时由于实现了ISerializable接口,因此会调用构造方法,这个也是必须要加的,假如不加的话会报错
OnDeserialized(反序列化结束)
04.3-纯注解模式 当我们不继承ISerializable接口后,我们会发现他的流程变得更加简单了。 它不会调用GetObjectData也不会实例化对象。单纯只执行我们上面说的基本四个生命周期。
04.4-SerializationInfo 在上述流程中我们经常会见到SerializationInfo这个类,那么这个类有什么用呢? 首先它的成员变量就是我们序列化对象的成员属性、类型、数量和名称。其次他给定了我们获取和修改成员属性的方法。这一点在上述的GetObjectData也看到了。
05-Dotnet的命令执行 在ASP.NET CORE里,命令执行的方式比较严格。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 using System.Diagnostics;namespace DonNET_Deserialization ;public class Command { public static void Main (string [] args ) { var start = Process.Start("cmd.exe" , "/c calc" ); var process = new Process(); process.StartInfo.FileName = "cmd.exe" ; process.StartInfo.Arguments = "/c calc" ; process.Start(); } }
在csharp中调用系统命令必须指定你的filename和argument,而不是单单输入一个calc那么简单。假如我们需要获取命令执行的回显可以这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 using System;using System.Diagnostics;namespace DonNET_Deserialization ;public class Command { public static void Main (string [] args ) { var process = new Process(); process.StartInfo.FileName = "cmd.exe" ; process.StartInfo.Arguments = "/c whoami" ; process.StartInfo.RedirectStandardOutput = true ; process.StartInfo.UseShellExecute = false ; process.Start(); var processStandardOutput = process.StandardOutput.ReadToEnd(); Console.WriteLine(processStandardOutput); } }
其中RedirectStandardOutput和UseShellExecute需要注意一下
RedirectStandardOutput:
当设置为 true 时,表示重定向标准输出流,即将进程的标准输出流(通常是在控制台窗口中显示的信息)连接到 Process.StandardOutput 流中,以便你的程序可以读取进程的输出。
UseShellExecute:
当设置为 false 时,表示不使用操作系统的 shell 启动进程。相反,它允许你直接启动可执行文件,命令行等,而不需要借助 shell。
06-ObjectDataProvider 先介绍一下这个类,他是我们gadgets其中的一环,我们需要了解一下他为什么可以命令执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 using System;using System.Diagnostics;using System.Windows.Data;namespace DonNET_Deserialization ;public class Command { public static void Main (string [] args ) { var objectDataProvider = new ObjectDataProvider(); objectDataProvider.MethodName = "Start" ; objectDataProvider.MethodParameters.Add("cmd.exe" ); objectDataProvider.MethodParameters.Add("/c calc" ); objectDataProvider.ObjectInstance = new Process(); } }
运行上述实例后就是可以命令执行。流程如下。
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 public object ObjectInstance { get { return this ._instanceProvider == null ? this ._objectInstance : (object ) this ._instanceProvider; } set { if (this ._mode == ObjectDataProvider.SourceMode.FromType) throw new InvalidOperationException(System.Windows.SR.Get("ObjectDataProviderCanHaveOnlyOneSource" )); this ._mode = value == null ? ObjectDataProvider.SourceMode.NoSource : ObjectDataProvider.SourceMode.FromInstance; if (this .ObjectInstance == value ) return ; if (value != null ) { this ._constructorParameters.SetReadOnly(true ); this ._constructorParameters.ClearInternal(); } else this ._constructorParameters.SetReadOnly(false ); value = this .TryInstanceProvider(value ); if (!this .SetObjectInstance(value ) || this .IsRefreshDeferred) return ; this .Refresh(); } }
在Objectdataprovideer的属性ObjectInstance的Setter方法存在一个Refresh刷新 然后会调用Queryworkr方法 然后会调用InvokeMethodOnInstance方法 最后执行了命令。也就是核心是该类的setter方法。
07-小结