C#中深度解析BinaryFormatter序列化生成的二进制文件
C#中深度解析BinaryFormatter序列化生成的二进制文件
BinaryFormatter序列化时,对象必须有 可序列化特性[Serializable]
一.新建窗体测试程序BinaryDeepAnalysisDemo,将默认的Form1重命名为FormBinaryDeepAnalysis
二.新建测试类Test
Test.cs源程序如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BinaryDeepAnalysisDemo
{
/// <summary>
/// 一定要标记该类是可序列化的
/// </summary>
[Serializable]
public class Test
{
public byte ByteData { get; set; }
public short Int16Data { get; set; }
public int Id { get; set; }
public long Int64Data { get; set; }
public float FloatData { get; set; }
public double DoubleData { get; set; }
public string TestName { get; set; }
}
}
三.新建字典与类相互转化类ConversionUtil:
ConversionUtil.cs源程序如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace BinaryDeepAnalysisDemo
{
/// <summary>
/// 字典Dictionary与自定义类CustomClass属性之间的转化
/// 斯内科 2024-05-10
/// </summary>
public class ConversionUtil
{
/// <summary>
/// 将一个实体类的属性转化为键值对集合,键为属性名PropertyName,值为对应的值
/// </summary>
/// <typeparam name="T">一种自定义类,该类必须已实例化</typeparam>
/// <param name="obj">实例化后的类对象</param>
/// <returns>返回转化后的字典</returns>
public static Dictionary<string, object> CustomClassToDictionary<T>(T obj) where T : class, new()
{
Dictionary<string, object> dict = new Dictionary<string, object>();
Type type = typeof(T);
PropertyInfo[] propertyInfos = type.GetProperties();
for (int i = 0; i < propertyInfos.Length; i++)
{
string key = propertyInfos[i].Name;
object val = propertyInfos[i].GetValue(obj);
dict.Add(key, val);
}
return dict;
}
/// <summary>
/// 将字典转化为实例化类,如果类的属性名称在字典中存在该键key,就使用该键对应的值为类的属性key赋值【注意,转化可能因值的类型、范围等不同而导致失败】
/// 如果类的属性名称在字典中不存在该键key,则不进行赋值,只是按属性对应类型的默认值处理
/// </summary>
/// <typeparam name="T">要转化的目标类型</typeparam>
/// <param name="dict">键值对字典</param>
/// <returns>要获取的目标实例化类对象,如果字典为空,则按类的默认值处理</returns>
public static T DictionaryToCustomClass<T>(Dictionary<string, object> dict) where T : class
{
T obj = default(T);
if (dict == null || dict.Count == 0)
{
return obj;
}
Type type = typeof(T);
obj = (T)Activator.CreateInstance(type);
PropertyInfo[] propertyInfos = type.GetProperties();
for (int i = 0; i < propertyInfos.Length; i++)
{
string key = propertyInfos[i].Name;
if (dict.ContainsKey(key))
{
propertyInfos[i].SetValue(obj, dict[key]);
}
}
return obj;
}
}
}
四.测试窗体设计器相关代码如下
文件FormBinaryDeepAnalysis.Designer.cs
namespace BinaryDeepAnalysisDemo
{
partial class FormBinaryDeepAnalysis
{
/// <summary>
/// 必需的设计器变量。
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 清理所有正在使用的资源。
/// </summary>
/// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows 窗体设计器生成的代码
/// <summary>
/// 设计器支持所需的方法 - 不要修改
/// 使用代码编辑器修改此方法的内容。
/// </summary>
private void InitializeComponent()
{
this.panel1 = new System.Windows.Forms.Panel();
this.rtxtMessage = new System.Windows.Forms.RichTextBox();
this.btnSerial = new System.Windows.Forms.Button();
this.btnDeserial = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// panel1
//
this.panel1.Location = new System.Drawing.Point(12, 12);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(300, 532);
this.panel1.TabIndex = 0;
//
// rtxtMessage
//
this.rtxtMessage.Location = new System.Drawing.Point(511, 12);
this.rtxtMessage.Name = "rtxtMessage";
this.rtxtMessage.ReadOnly = true;
this.rtxtMessage.Size = new System.Drawing.Size(482, 532);
this.rtxtMessage.TabIndex = 1;
this.rtxtMessage.Text = "";
//
// btnSerial
//
this.btnSerial.Location = new System.Drawing.Point(360, 78);
this.btnSerial.Name = "btnSerial";
this.btnSerial.Size = new System.Drawing.Size(63, 23);
this.btnSerial.TabIndex = 2;
this.btnSerial.Text = "序列化";
this.btnSerial.UseVisualStyleBackColor = true;
this.btnSerial.Click += new System.EventHandler(this.btnSerial_Click);
//
// btnDeserial
//
this.btnDeserial.Location = new System.Drawing.Point(360, 155);
this.btnDeserial.Name = "btnDeserial";
this.btnDeserial.Size = new System.Drawing.Size(63, 23);
this.btnDeserial.TabIndex = 3;
this.btnDeserial.Text = "反序列化";
this.btnDeserial.UseVisualStyleBackColor = true;
this.btnDeserial.Click += new System.EventHandler(this.btnDeserial_Click);
//
// FormBinaryDeepAnalysis
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1001, 556);
this.Controls.Add(this.btnDeserial);
this.Controls.Add(this.btnSerial);
this.Controls.Add(this.rtxtMessage);
this.Controls.Add(this.panel1);
this.Name = "FormBinaryDeepAnalysis";
this.Text = "序列化生成的二进制进行深度解析";
this.Load += new System.EventHandler(this.FormBinaryDeepAnalysis_Load);
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.RichTextBox rtxtMessage;
private System.Windows.Forms.Button btnSerial;
private System.Windows.Forms.Button btnDeserial;
}
}
五.窗体FormBinaryDeepAnalysis相关示例代码如下
文件FormBinaryDeepAnalysis.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace BinaryDeepAnalysisDemo
{
public partial class FormBinaryDeepAnalysis : Form
{
public FormBinaryDeepAnalysis()
{
InitializeComponent();
}
/// <summary>
/// 显示文本
/// </summary>
/// <param name="contents"></param>
private void DisplayMessage(string contents)
{
if (!IsHandleCreated)
return;
this.BeginInvoke(new Action(() =>
{
if (rtxtMessage.TextLength >= 20480)
{
rtxtMessage.Clear();
}
rtxtMessage.AppendText($"{contents}\n");
rtxtMessage.ScrollToCaret();
}));
}
private void FormBinaryDeepAnalysis_Load(object sender, EventArgs e)
{
Test test = new Test()
{
ByteData = 234,
Int16Data = 23456,
Id = 12345678,
Int64Data = 9876543210123L,
FloatData = 60.25F,
DoubleData = -87654321.3125,
TestName = "ABC123abc"
};
Dictionary<string, object> dict = ConversionUtil.CustomClassToDictionary(test);
for (int i = 0; i < dict.Count; i++)
{
KeyValuePair<string, object> keyValuePair = dict.ElementAt(i);
Label label = new Label();
label.Name = $"lbl{keyValuePair.Key}";
label.Text = keyValuePair.Key;
label.Location = new Point(3, 3 + 30 * i);
TextBox textBox = new TextBox();
textBox.Name = $"txb{keyValuePair.Key}";
textBox.Text = Convert.ToString(keyValuePair.Value);
textBox.Location = new Point(100, 3 + 30 * i);
textBox.Size = new Size(160, 25);
textBox.Tag = keyValuePair.Key;
panel1.Controls.AddRange(new Control[] { label, textBox });
}
}
private void btnSerial_Click(object sender, EventArgs e)
{
try
{
Dictionary<string, object> dict = new Dictionary<string, object>();
for (int i = 0; i < panel1.Controls.Count; i++)
{
Control control = panel1.Controls[i];
if (control is TextBox)
{
object val = control.Text;
switch (Convert.ToString(control.Tag))
{
case "ByteData":
val = byte.Parse(control.Text);
break;
case "Int16Data":
val = short.Parse(control.Text);
break;
case "Id":
val = int.Parse(control.Text);
break;
case "Int64Data":
val = long.Parse(control.Text);
break;
case "FloatData":
val = float.Parse(control.Text);
break;
case "DoubleData":
val = double.Parse(control.Text);
break;
}
dict.Add(Convert.ToString(control.Tag), val);
}
}
Test test = ConversionUtil.DictionaryToCustomClass<Test>(dict);
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter binaryFormatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
string path = $"{AppDomain.CurrentDomain.BaseDirectory}test.bin";
using (System.IO.FileStream stream = new System.IO.FileStream(path, System.IO.FileMode.Create))
{
binaryFormatter.Serialize(stream, test);
}
byte[] buffer = System.IO.File.ReadAllBytes(path);
DisplayMessage($"长度:{buffer.Length}\n字节流:\n{string.Join(",", buffer)}");
DisplayMessage("------打印每个属性的字节流编码,以及查找序列化的二进制数据------");
SeekData(TypeCode.Byte, "ByteData", test.ByteData, buffer);
SeekData(TypeCode.Int16, "Int16Data", test.Int16Data, buffer);
SeekData(TypeCode.Int32, "Id", test.Id, buffer);
SeekData(TypeCode.Int64, "Int64Data", test.Int64Data, buffer);
SeekData(TypeCode.Single, "FloatData", test.FloatData, buffer);
SeekData(TypeCode.Double, "DoubleData", test.DoubleData, buffer);
SeekData(TypeCode.String, "TestName", test.TestName, buffer);
DisplayMessage("------打印可转换为ASCII的字符,如果是看不到的特殊字符,显示源字节------");
string ascii = "";
for (int i = 0; i < buffer.Length; i++)
{
if (buffer[i] >= 32 && buffer[i] <= 126)
{
ascii += (char)buffer[i];
}
else
{
ascii += $"【{buffer[i]}】";
}
}
DisplayMessage(ascii);
}
catch (Exception ex)
{
DisplayMessage($"序列化时出现异常:【{ex.Message}】");
}
}
/// <summary>
/// 从字节流中查找数据
/// </summary>
/// <param name="typeCode"></param>
/// <param name="propertyName"></param>
/// <param name="val"></param>
/// <param name="buffer"></param>
private void SeekData(TypeCode typeCode, string propertyName, object val, byte[] buffer)
{
int byteCount = 0;//类型所占用的字节个数
byte[] dataBuffer = new byte[0];
switch (typeCode)
{
case TypeCode.Byte:
byteCount = sizeof(byte);//类型所占用的字节个数
dataBuffer = new byte[] { (byte)val };
break;
case TypeCode.Int16:
byteCount = sizeof(short);
dataBuffer = BitConverter.GetBytes((short)val);
break;
case TypeCode.Int32:
byteCount = sizeof(int);
dataBuffer = BitConverter.GetBytes((int)val);
break;
case TypeCode.Int64:
byteCount = sizeof(long);
dataBuffer = BitConverter.GetBytes((long)val);
break;
case TypeCode.Single:
byteCount = sizeof(float);
dataBuffer = BitConverter.GetBytes((float)val);
break;
case TypeCode.Double:
byteCount = sizeof(double);
dataBuffer = BitConverter.GetBytes((double)val);
break;
default:
dataBuffer = Encoding.ASCII.GetBytes(Convert.ToString(val));
byteCount = dataBuffer.Length;
break;
}
DisplayMessage($"{propertyName}属性的值为【{val}】,字节流【{string.Join(",", dataBuffer)}】");
List<int> listIndex = new List<int>();
for (int i = 0; i < buffer.Length - byteCount; i++)
{
bool[] bArray = new bool[byteCount];
for (int cnt = 0; cnt < byteCount; cnt++)
{
bArray[cnt] = buffer[i + cnt] == dataBuffer[cnt];
}
//查找所有元素是否都为true
if (Array.FindAll(bArray, x => x).Length == byteCount)
{
listIndex.Add(i);
}
}
DisplayMessage($"\x20\x20\x20【{propertyName}】数据的地址查找到的起始索引有【{string.Join(",", listIndex)}】");
}
private void btnDeserial_Click(object sender, EventArgs e)
{
Test test = null;
string path = $"{AppDomain.CurrentDomain.BaseDirectory}test.bin";
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter binaryFormatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
using (System.IO.FileStream stream = new System.IO.FileStream(path, System.IO.FileMode.Open))
{
test = binaryFormatter.Deserialize(stream) as Test;
}
if(test == null)
{
return;
}
Dictionary<string, object> dict = ConversionUtil.CustomClassToDictionary(test);
for (int i = 0; i < panel1.Controls.Count; i++)
{
Control control = panel1.Controls[i];
if (control is TextBox)
{
control.Text = "";
switch (Convert.ToString(control.Tag))
{
case "ByteData":
control.Text = Convert.ToString(test.ByteData);
break;
case "Int16Data":
control.Text = Convert.ToString(test.Int16Data);
break;
case "Id":
control.Text = Convert.ToString(test.Id);
break;
case "Int64Data":
control.Text = Convert.ToString(test.Int64Data);
break;
case "FloatData":
control.Text = Convert.ToString(test.FloatData);
break;
case "DoubleData":
control.Text = Convert.ToString(test.DoubleData);
break;
default:
control.Text = test.TestName;
break;
}
}
}
}
}
}
六.窗体运行如图:
我们可以发现 生成的字节码中 含有 程序集和类信息,以及属性名和对应的值
程序集和版本信息
类和属性字段以及相关数据类型信息
<TestName> 这里的TestName就是属性字段,如果需要获取该字段的类型,可以使用System.Reflection反射来获取具体的数据类型
具体数值区域
这里的数据大多都是使用BitConverter.GetBytes(数据值)进行字节流转化
七.BinaryFormatter序列化的不安全性
如果将二进制文件读取出来,然后篡改指定的字节码,生成新的二进制文件,然后重新读取,结果就会发现数据被篡改了.很多单机游戏的存档文件修改就是这么干的.
这也是不提倡使用BinaryFormatter类进行序列化的原因,可能被劫持篡改二进制文件,因此在最新的net 5.0中会被标记为不安全的和过时的原因