[DotnetSec]初探反序列化

[dotnet-Sec]初探反序列化

参考Github上y4✌的开源笔记,狠狠学!

环境搭建

.NET:5.0

IDE:Rider(JB家族)

新建项目

选择.NET Core(支持跨平台)下的控制台应用程序,然后创建

image-20240221163113770

这是接触到的关于dotnet的第一个反序列化demo,使用的是BinaryFormatter生成二进制流

// Disable the warning.
#pragma warning disable SYSLIB0011

using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

namespace dotnet_sec_101
{
    [Serializable]
    public class DemoObject
    {
        public int n1;
        [NonSerialized] public int n2;
        public string str;
    }

    class Tester
    {
        public static void BinaryFomatterSerialize(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 BinaryFomatterDeserialFromFile(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
            {
                DemoObject demoObject = new DemoObject();
                demoObject.n1 = 1;
                demoObject.n2 = 2;
                demoObject.str = "hack";
                
                BinaryFomatterSerialize("ser.bin", demoObject);
                DemoObject _demoObject = (DemoObject)BinaryFomatterDeserialFromFile("ser.bin");
                
                Console.WriteLine($"n1: {_demoObject.n1}");
                Console.WriteLine($"NonSer n2: {_demoObject.n2}");
                Console.WriteLine($"str: {_demoObject.str}");
            }
            catch (Exception e) {
                Console.WriteLine(e.Message);
            }
            
            Console.ReadKey();
    
        }
    
    }
}
// Re-enable the warning.
#pragma warning restore SYSLIB0011

由于.net的高版本问题,需要在csproj项目文件中添加如下条目

image-20240221163800130

运行,可以发现加了[NonSerialized]特性的变量n2未参与序列化,于是在反序列化后也就没有对应值,默认为0

image-20240221163827075

我们查看使用BinaryFormatter生成的二进制文件ser.bin,开头为00 01 00 00

image-20240221164027350

IFormatter接口

我们查看底层BinaryFormatter的实现,实现了IFormatter接口

(后面测试在.net framework平台下还继承了另一个接口IRemotingFormatter

IRemotingFormatter是用于远程调用的RPC接口

image-20240221224056030

image-20240221205733348

最终的IFormatter接口如下:

  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 SurrogateSelector 序列化代理选择器 接管formatter的序列化或反序列化处理
SerializationBinder Binder 用于控制在序列化和反序列化期间使用的实际类型
StreamingContext Context 序列化流上下文 其中states字段包含了序列化的来源和目的地

通过这三个字段,我们可以控制序列化和反序列化时数据的类型、值以及其他信息。

序列化和反序列化的生命周期

生命周期如下:

  1. 首先确定formatter是否有代理选择器,如果有则检查代理选择器要处理的对象类型是否和给定的对象类型一致,如果一致,代理选择器会调用ISerializable.GetObjectData()
  2. 如果没有代理选择器,或者代理选择器不处理该对象类型,则检查对象是否有[Serializable]特性。如果不能序列化则抛出异常。
  3. 检查该对象是否实现ISerializable接口,如果实现就调用其GetObjectData方法。
  4. 如果没实现ISerializable接口就使用默认的序列化策略,序列化所有没标记[NonSerialized]的字段。

image-20240222140227018

ISerializationSurrogate代理器模式

首先看下它的接口定义:

需要在继承接口中实现如下函数:GetObjectDataSetObjectData

using System.Runtime.InteropServices;
using System.Security;

#nullable disable
namespace System.Runtime.Serialization
{
  [ComVisible(true)]
  public interface ISerializationSurrogate
  {
    [SecurityCritical] // 确保只有具有足够权限的代码才能调用或访问被标记的代码。
    void GetObjectData(object obj, SerializationInfo info, StreamingContext context);
      // 序列化的时候被调用(压缩对象前需要get data)
      

    [SecurityCritical]
    object SetObjectData(		// 反序列化时调用(恢复对象时要set data)
      object obj,
      SerializationInfo info,
      StreamingContext context,
      ISurrogateSelector selector);
  }
}

image-20240222114037185

非代理器 + 继承ISerializable

接口定义如下:

#nullable disable
namespace System.Runtime.Serialization
{
  [ComVisible(true)]
  public interface ISerializable
  {
    [SecurityCritical]
    void GetObjectData(SerializationInfo info, StreamingContext context);
  }
}

注释掉BinaryFormatter设置代理器的语句即可

image-20240222113742872

然后运行可以发现

image-20240222113958495

注解模式(使用四个回调事件)

这个没啥好说的

特性 调用关联的方法时 典型用法
OnDeserializingAttribute 反序列化之前 初始化可选字段的默认值。
OnDeserializedAttribute 反序列化之后 根据其他字段的内容修改可选字段值。
OnSerializingAttribute 序列化之前 准备序列化。 例如,创建可选数据结构。
OnSerializedAttribute 序列化之后 记录序列化事件。

演示demo

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Permissions;


namespace NetSerializer
{
    [Serializable]
    public class MyObject : ISerializable
    {
        public string str { get; set; }

        public MyObject()
        {
            
        }

        // 实现Iserializable接口的类必须包含有序列化构造函数, 否则会出错
        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(SerializationInfo info, StreamingContext context)");
            info.AddValue("str", str, typeof(string));
        }
        
        // 四个回调事件 以及对应的特性
        [OnDeserializing] // 反序列化之前
        private void TestForOnDeserializing(StreamingContext sc)
        {
            // 打印回调函数调用时的上下文
            //Console.WriteLine(sc.ToString());
            Console.WriteLine("OnDeserializing Test");
        }

        [OnDeserialized]
        private void TestForOnDeserialized(StreamingContext sc)
        {
            //Console.WriteLine(sc.ToString());
            Console.WriteLine("OnDeserialized Test");
        }

        [OnSerializing]
        private void TestForOnSerializing(StreamingContext sc)
        {
            //Console.WriteLine(sc.ToString());
            Console.WriteLine("OnSerializing Test");
        }

        [OnSerialized]
        private void TestForOnSerialized(StreamingContext sc)
        {
            //Console.WriteLine(sc.ToString());
            Console.WriteLine("OnSerialized Test");
        }
        
    }

    
    
    // 声明代理选择器
    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 Tester
    {
        static void Main(string[] args)
        {
            try
            {
                MyObject myObject = new MyObject();
                myObject.str = "hello";

                using (MemoryStream memoryStream = new MemoryStream())
                {
                    // 构建formatter
                    BinaryFormatter binaryFormatter = new BinaryFormatter();
                    
                    // 设置序列化代理选择器
                    SurrogateSelector ss = new SurrogateSelector();
                    ss.AddSurrogate(typeof(MyObject), binaryFormatter.Context, new MySerializationSurrogate());

                    // 将序列化代理选择器赋给formatter 
                    binaryFormatter.SurrogateSelector = ss;
                    
                    // 序列化
                    binaryFormatter.Serialize(memoryStream, myObject);
                    
                    // 重置stream
                    memoryStream.Position = 0;
                    myObject = null;
                    
                    // 反序列化
                    myObject = (MyObject)binaryFormatter.Deserialize(memoryStream);
                    Console.WriteLine(myObject.str);
                }
                
                

            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
                      
        }
    }
}

生命周期小结

image-20210420105228965

SerializationInfo

在上面生命周期的探讨中,使用到了SerializationInfo作为参数

存储序列化过程中的信息

image-20240222130807788

其中上述的AddValue函数的底层实现:

使用m_membersm_datam_types存储变量名、值、类型

    internal void AddValueInternal(string name, object value, Type type)
    {
      if (this.m_nameToIndex.ContainsKey(name))
        throw new SerializationException(Environment.GetResourceString("Serialization_SameNameTwice"));
      this.m_nameToIndex.Add(name, this.m_currMember);
      if (this.m_currMember >= this.m_members.Length)
        this.ExpandArrays();
      this.m_members[this.m_currMember] = name;
      this.m_data[this.m_currMember] = value;
      this.m_types[this.m_currMember] = type;
      ++this.m_currMember;
    }

​ 在不使用代理器模式的情况且在反序列化的过程中,通过调试可以发现,在CompleteISerializableObject() 中调用了runtimeConstructorInfo.SerializationInvoke(obj, info, context),这也能说明了SerializationInfo的作用

image-20240222134147131

​ 然后进入到自定义的构造函数,完整栈帧如下

Main Thread
ObjectManager.CompleteISerializableObject()
ObjectManager.FixupSpecialObject()
ObjectManager.DoFixups()
ObjectReader.Deserialize()
BinaryFormatter.Deserialize()
BinaryFormatter.Deserialize()
Tester.Main()

internal void CompleteISerializableObject(
  object obj,
  SerializationInfo info,
  StreamingContext context)
{
  if (obj == null)
    throw new ArgumentNullException(nameof (obj));
  RuntimeType t = obj is ISerializable ? (RuntimeType) obj.GetType() : throw new ArgumentException(Environment.GetResourceString("Serialization_NotISer"));
  RuntimeConstructorInfo runtimeConstructorInfo;
  try
  {
    runtimeConstructorInfo = !(t == ObjectManager.TypeOfWindowsIdentity) || !this.m_isCrossAppDomain ? ObjectManager.GetConstructor(t) : WindowsIdentity.GetSpecialSerializationCtor();
  }
  catch (Exception ex)
  {
    throw new SerializationException(Environment.GetResourceString("Serialization_ConstructorNotFound", (object) t), ex);
  }
    // 调用构造函数
  runtimeConstructorInfo.SerializationInvoke(obj, info, context);
}

​ 相应的在序列化过程中可以找到调用GetObjectData的时机:

Main Thread
WriteObjectInfo.InitSerialize()
WriteObjectInfo.Serialize()
ObjectWriter.Serialize()
BinaryFormatter.Serialize()
BinaryFormatter.Serialize()
Tester.Main()

image-20240222140013506

posted @   Icfh  阅读(33)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示