没有对象定义的二进制反序列化

6
我正在尝试读取一个二进制序列化对象,但我没有这个对象的定义/源代码。我瞥了一眼文件,看到了属性名称,因此我手动重新创建了该对象(我们称之为SomeDataFormat)。
最终得到的是:
public class SomeDataFormat // 16 field
{
    public string Name{ get; set; }
    public int Country{ get; set; } 
    public string UserEmail{ get; set; }
    public bool IsCaptchaDisplayed{ get; set; }
    public bool IsForgotPasswordCaptchaDisplayed{ get; set; }
    public bool IsSaveChecked{ get; set; }
    public string SessionId{ get; set; } 
    public int SelectedLanguage{ get; set; } 
    public int SelectedUiCulture{ get; set; } 
    public int SecurityImageRefId{ get; set; } 
    public int LogOnId{ get; set; } 
    public bool BetaLogOn{ get; set; } 
    public int Amount{ get; set; }
    public int CurrencyTo{ get; set; }
    public int Delivery{ get; set; } 
    public bool displaySSN{ get; set; }
}   

现在我可以像这样反序列化它:

BinaryFormatter formatter = new BinaryFormatter();  
formatter.AssemblyFormat = FormatterAssemblyStyle.Full; // original uses this       
formatter.TypeFormat = FormatterTypeStyle.TypesWhenNeeded; // this reduces size
FileStream readStream = new FileStream("data.dat", FileMode.Open);
SomeDataFormat data = (SomeDataFormat) formatter.Deserialize(readStream);

首先可疑的事情是,在反序列化的data对象中,只有2个字符串(SessionIdUserEmail)具有值。其他属性为null或仅为0。这可能是有意的,但我仍然怀疑在反序列化过程中出现了问题。
第二个可疑的事情是,如果我重新序列化此对象,最终得到的文件大小会不同。原始大小为695字节,而重新序列化后的对象大小为698字节,相差3字节。我应该得到与原始文件相同的文件大小。
查看原始文件和新的(重新序列化的)文件:
原始序列化文件:(zoom) enter image description here 重新序列化文件:(zoom) enter image description here 如您所见,在标题部分之后,数据似乎是以不同的顺序出现的。例如,您可以看到电子邮件和sessionID不在同一位置。
更新:Will提醒我,“PublicKeyToken=null”之后的字节也不同。(03 <-> 05)
Q1:为什么两个文件中的值顺序不同?
Q2:相比2个序列化对象,为什么会有额外的3个字节?
Q3:我错过了什么?我该如何处理?
感谢您的任何帮助。

有点相关的问题: 1 2 3


1
你应该检查 Data_reSerialized.dat 是否能够进行反序列化,并报告它所产生的序列化大小;即 Data_reReSerialized.dat 的大小是多少? - Mark Hurd
1
你的意思是当我反序列化Data_reReSerialized.dat文件时,它的大小是多少?我会在今天晚些时候回报结果。 - Dominik Antal
1
@MarkHurd 我成功地重新序列化了对象,现在它只比它应该的大3个字节。我根本没有操作数据,一定是我的对象定义出了问题,或者我错过了某个选项。我很快就会发布图片。 - Dominik Antal
2
我假设你已经看过右侧第一个相关问题:如何分析二进制序列化流的内容? - Mark Hurd
5个回答

8
两个文件中的值为什么顺序不同? 这是因为成员顺序不是基于声明顺序的。http://msdn.microsoft.com/en-us/library/424c79hc.aspx GetMembers方法不会按特定顺序(如字母或声明顺序)返回成员。您的代码不能依赖于成员返回的顺序,因为该顺序会变化。

.

为什么两个序列化对象相比会多出3个字节?

首先,TypeFormat格式应该是'TypesAlways'而不是'TypesWhenNeeded',这就是有这么多差异的原因。例如,'=null'后面的05变成03就是由于此原因。

其次,你没有正确的类型。查看ILSpy中的BinaryFormatter和十六进制转储可以发现,标记为'int'的成员实际上是'string'。

public class SomeDataFormat // 16 field
{
    public string Name { get; set; }
    public string Country { get; set; } 
    public string UserEmail{ get; set; }
    public bool IsCaptchaDisplayed{ get; set; }
    public bool IsForgotPasswordCaptchaDisplayed{ get; set; }
    public bool IsSaveChecked{ get; set; }
    public string SessionId{ get; set; } 
    public string SelectedLanguage{ get; set; } 
    public string SelectedUiCulture{ get; set; } 
    public string SecurityImageRefId{ get; set; } 
    public string LogOnId{ get; set; } 
    public bool BetaLogOn{ get; set; } 
    public string Amount{ get; set; }
    public string CurrencyTo{ get; set; }
    public string Delivery{ get; set; } 
    public bool displaySSN{ get; set; }
}

我缺少什么?我应该怎么做?

我没有看到使用给定的BinaryFormatter的方法。您可以反编译/逆转BinaryFormatter的工作方式。


6
因为这可能会对某些人有帮助,所以我决定写这篇关于“序列化的.NET对象的二进制格式是什么样的,我们如何正确地解释它”的文章。

我所有的研究都基于.NET Remoting: Binary Format Data Structure规范。



示例类:

为了举一个实际的例子,我创建了一个简单的类叫做A,它包含两个属性,一个字符串和一个整数值,它们分别被称为SomeStringSomeValue

A长这样:

[Serializable()]
public class A
{
    public string SomeString
    {
        get;
        set;
    }

    public int SomeValue
    {
        get;
        set;
    }
}

我使用了BinaryFormatter进行序列化:
BinaryFormatter bf = new BinaryFormatter();
StreamWriter sw = new StreamWriter("test.txt");
bf.Serialize(sw.BaseStream, new A() { SomeString = "abc", SomeValue = 123 });
sw.Close();

可以看到,我传递了一个新的类实例 A,其中包含值 abc123示例结果数据: 如果我们在十六进制编辑器中查看序列化结果,会得到如下内容:

Example result data



让我们解释一下示例结果数据:

根据上述规范(这里是到PDF的直接链接:[MS-NRBF].pdf),流中的每个记录都由RecordTypeEnumeration标识。章节2.1.2.1 RecordTypeNumeration指出:

此枚举标识记录的类型。每个记录(除了MemberPrimitiveUnTyped)都以记录类型枚举开始。枚举的大小为一个字节。



SerializationHeaderRecord:

因此,如果我们回顾一下得到的数据,我们可以开始解释第一个字节:

SerializationHeaderRecord_RecordTypeEnumeration

2.1.2.1 RecordTypeEnumeration中所述,值为0的标识符用于指定2.6.1 SerializationHeaderRecord中的SerializationHeaderRecord

SerializationHeaderRecord记录必须是二进制序列化中的第一个记录。该记录包含格式的主版本和次版本以及顶级对象和标题的ID。

它由以下内容组成:

  • RecordTypeEnum(1字节)
  • RootId(4字节)
  • HeaderId(4字节)
  • MajorVersion(4字节)
  • MinorVersion(4字节)



有了这些知识,我们可以解释包含17个字节的记录:

SerializationHeaderRecord_Complete

00代表RecordTypeEnumeration,在我们的情况下是SerializationHeaderRecord

01 00 00 00代表RootId

如果序列化流中不存在BinaryMethodCall或BinaryMethodReturn记录,则此字段的值必须包含序列化流中包含的Class、Array或BinaryObjectString记录的ObjectId。

所以在我们的情况下,这应该是具有值1ObjectId(因为数据使用little-endian进行序列化),我们希望再次看到它;-)

FF FF FF FF代表HeaderId

01 00 00 00代表MajorVersion

00 00 00 00代表MinorVersion



BinaryLibrary:

按照规定,每个记录必须以RecordTypeEnumeration开始。由于最后一个记录已经完成,我们必须假设一个新的记录开始了。

让我们解释下一个字节:

BinaryLibraryRecord_RecordTypeEnumeration

正如我们所看到的,在我们的示例中,SerializationHeaderRecord之后是BinaryLibrary记录:

BinaryLibrary记录将INT32 ID(如[MS-DTYP]第2.2.22节中所指定的)与库名称相关联。这允许其他记录通过使用ID引用库名称。当存在多个记录引用相同的库名称时,此方法可以减少线路大小。

它包括:
  • RecordTypeEnum(1字节)
  • LibraryId(4字节)
  • LibraryName(可变数量的字节(即LengthPrefixedString))



2.1.1.6 LengthPrefixedString中所述...

LengthPrefixedString表示字符串值。该字符串以UTF-8编码的字节数组长度为前缀。长度在一个最小为1个字节、最大为5个字节的可变长度字段中进行编码。为了最小化线路大小,长度被编码为可变长度字段。

在我们的简单示例中,长度始终使用 1字节 进行编码。有了这个知识,我们可以继续解释流中的字节:

BinaryLibraryRecord_RecordTypeEnumeration_LibraryId

0C代表RecordTypeEnumeration,用于标识BinaryLibrary记录。

02 00 00 00代表LibraryId,在我们的情况下是2



现在是LengthPrefixedString

BinaryLibraryRecord_RecordTypeEnumeration_LibraryId_LibraryName

42代表包含LibraryNameLengthPrefixedString的长度信息。

在我们的情况下,42(十进制66)的长度信息告诉我们需要读取接下来的66个字节并将它们解释为LibraryName

如前所述,该字符串是UTF-8编码的,因此上面的字节结果可能类似于:_WorkSpace_,Version=1.0.0.0,Culture=neutral,PublicKeyToken=null



ClassWithMembersAndTypes:

同样,记录已经完整,因此我们解释下一个RecordTypeEnumeration

ClassWithMembersAndTypesRecord_RecordTypeEnumeration

05代表一个ClassWithMembersAndTypes记录。在 2.3.2.1 ClassWithMembersAndTypes 部分中说明:

ClassWithMembersAndTypes记录是最冗长的类记录之一。它包含有关成员的元数据,包括成员的名称和Remoting类型。它还包含引用类库名称的库ID。

它由以下内容组成:

  • RecordTypeEnum(1字节)
  • ClassInfo(可变字节数)
  • MemberTypeInfo(可变字节数)
  • LibraryId(4字节)



ClassInfo:

2.3.1.1 ClassInfo 中所述,该记录包含以下内容:

  • ObjectId(4字节)
  • Name(可变字节数(又是一个LengthPrefixedString))
  • MemberCount(4字节)
  • MemberNames(这是一系列LengthPrefixedString,其中项目数必须等于MemberCount字段中指定的值。)



回到原始数据,一步一步:

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId

"01 00 00 00"代表ObjectId。我们已经看到过这个,它被指定为SerializationHeaderRecord中的RootId

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name "0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41"代表使用LengthPrefixedString表示的类的Name。如前所述,在我们的示例中,字符串的长度由1字节定义,因此第一个字节0F指定必须读取和解码15个字节,使用UTF-8进行解码。结果看起来像这样:StackOverFlow.A - 因此显然我将StackOverFlow用作命名空间的名称。

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name_MemberCount "

02 00 00 00表示MemberCount,它告诉我们有2个成员,两个都用LengthPrefixedString表示,将会跟随。

第一个成员的名称: ClassWithMembersAndTypesRecord_MemberNameOne

1B 3C 53 6F 6D 65 53 74 72 69 6E 67 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64表示第一个MemberName1B再次是字符串的长度,长度为27个字节,结果类似于这样:<SomeString>k__BackingField

第二个成员的名称: ClassWithMembersAndTypesRecord_MemberNameTwo

1A 3C 53 6F 6D 65 56 61 6C 75 65 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64代表第二个MemberName,其中1A指定该字符串长度为26个字节。结果类似于这样:<SomeValue>k__BackingField



MemberTypeInfo:

ClassInfo之后是MemberTypeInfo

第2.3.1.2节 - MemberTypeInfo指出,该结构包含:

  • BinaryTypeEnums(可变长度)
一个 BinaryTypeEnumeration 值序列,表示正在传输的成员类型。数组必须满足以下条件:
  • 与 ClassInfo 结构的 MemberNames 字段具有相同数量的项。
  • 排序方式应使 BinaryTypeEnumeration 对应于 ClassInfo 结构的 MemberNames 字段中的成员名称。
根据 BinaryTpeEnum 的不同,AdditionalInfos 的长度可能会有所不同。 | BinaryTypeEnum | AdditionalInfos | |----------------+--------------------------| | Primitive | PrimitiveTypeEnumeration | | String | None |
因此,考虑到这一点,我们已经接近成功了...... 我们期望有 2 个 BinaryTypeEnumeration 值(因为在 MemberNames 中有 2 个成员)。
再次回到完整的 MemberTypeInfo 记录的原始数据:

ClassWithMembersAndTypesRecord_MemberTypeInfo

01代表第一个成员的BinaryTypeEnumeration,根据2.1.2.2 BinaryTypeEnumeration,我们可以期望得到一个String,并且它使用LengthPrefixedString表示。

00代表第二个成员的BinaryTypeEnumeration,同样地,根据规范,它是一个Primitive。如上所述,Primitive后面跟着额外的信息,在这种情况下是一个PrimitiveTypeEnumeration。这就是为什么我们需要读取下一个字节,即08,将其与2.1.2.3 PrimitiveTypeEnumeration中的表匹配,并惊讶地发现我们可以期望得到一个由4个字节表示的Int32,正如其他关于基本数据类型的文档中所述。



LibraryId:

MemberTypeInfo之后是LibraryId,由4个字节表示:

ClassWithMembersAndTypesRecord_LibraryId

02 00 00 00代表的是LibraryId,它的值为2。



数值:

2.3类记录中所述:

类成员的值必须作为记录序列化,遵循本节中指定的记录,如2.7节所述。记录的顺序必须与类信息(2.3.1.1节)结构中的MemberNames的顺序相匹配。

这就是为什么我们现在可以期望成员的值。

让我们看一下最后几个字节:

BinaryObjectStringRecord_RecordTypeEnumeration

06代表一个BinaryObjectString。它表示我们的SomeString属性的值(确切地说是<SomeString>k__BackingField)。

根据2.5.7 BinaryObjectString,它包含:

  • RecordTypeEnum(1个字节)
  • ObjectId(4个字节)
  • Value(可变长度,表示为LengthPrefixedString



因此,了解这些,我们可以清楚地确定

BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue

03 00 00 00 表示 ObjectId

03 61 62 63 表示 Value,其中03是字符串本身的长度,61 62 63 是对应的字节内容,可以解析为 abc

希望你还记得有第二个成员,一个 Int32。由于知道 Int32 使用 4 个字节表示,我们可以得出结论:

BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue_MemberTwoValue

必须是我们第二个成员的Value。十六进制的7B等于十进制的123,似乎符合我们的示例代码。

因此,这是完整的ClassWithMembersAndTypes记录: ClassWithMembersAndTypesRecord_Complete



MessageEnd:

MessageEnd_RecordTypeEnumeration

最后一个字节 0B 表示 MessageEnd 记录。

请参见 https://dev59.com/-XA75IYBdhLWcg3w4tR7#30176566 - malat

3

如果我没记错的话,二进制序列化器会转储关于对象类型名称和命名空间的一些信息。如果这些值与原始类类型和你的新“SomeDataFormat”不同,这可能解释了文件大小差异。

您尝试使用十六进制编辑器比较了这两个文件吗?


那是要检查的第一件事。我能够读取对象变量名称,所以我创建了一个包含这些属性的类。然后我尝试对其进行反序列化,当它抱怨“无法将int转换为bool”等时,实际上告诉了我它期望的数据类型。我更正了类型,然后它就成功地反序列化了。 - Dominik Antal
我对binaryFormatter属性进行了一些调整,并发现我可以排除类型信息,这样在重新序列化时可以得到698个字节而不是之前的672个字节,因此现在只有3个额外的字节。另外,在十六进制编辑器中查看原始和新序列化的对象,我发现数据的顺序不同。我可能会制作一些图片。 - Dominik Antal

2

当你进行反序列化时,有些东西会被成功地向上转型。例如:

public class SomeClass()
{
   public short SomeProperty {get;set;}
}

将反序列化为
public class SomeClass()
{
   public long SomeProperty {get;set;}
}

但是,如果你序列化第二个SomeClass(即带有long的那个),它将导致与带有short的SomeClass序列化结果不同。在这种特定情况下,差异为6个字节。

更新:

反序列化成一个通用对象,然后使用反射来获取类型。你可能需要对复杂对象进行递归和特殊处理。

using (var fileStream = new FileStream("TestFormatter.dat", FileMode.Open))
        {
            var binaryFormatter = new BinaryFormatter();
            var myObject = binaryFormatter.Deserialize(fileStream);
            var objectProperties = myObject.GetType().GetProperties();
            foreach (var property in objectProperties)
            {
                var propertyTypeName = property.PropertyType.Name; //This will tell you the property Type Name. I.e. string, int64 (long)
            }                
        }

我可以假设反序列化的文件包含原始对象中使用的所有字段名称吗?还是只有非空的、使用过的字段名称被序列化到那个文件中?另外,有没有办法确定SomeProperty是什么类型? - Dominik Antal
1
默认情况下,BinaryFormatter将包括所有字段,即使它们为null。根据对象的复杂程度,您可能可以在通用对象图上使用反射。我会更新我的答案,介绍如何做到这一点。 - cgotberg
我尝试了你的反射方法,它给了我与我现在使用的相同类型。所以我想这意味着我被卡住了。 - Dominik Antal
1
我已经没有更多的想法了。也许你可以想出一种方法来获取被序列化的原始对象。如果只序列化通用对象,你最终会遇到相同的大小问题吗? - cgotberg

1
剩余的差异可能来自于您的类缺少属性。尝试这样做:
[StructLayout(LayoutKind.Sequential, Pack=1)]
public class SomeDataFormat // 16 field
{
   ...

Will解决了这个问题,不过这是一个好主意,谢谢 :) - Dominik Antal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接