Because it may be interesting for someone, I decided to make this message about . What does the binary format of serialized .NET objects look like and how can we interpret it correctly?
I based all my research on . NET Remoting Specification: Binary Format Data Structure .
Class class:
To have a working example, I created a simple class called A that contains 2 properties, one row and one integer value, they are called SomeString and SomeValue .
Class A as follows:
[Serializable()] public class A { public string SomeString { get; set; } public int SomeValue { get; set; } }
For serialization, I used BinaryFormatter , of course:
BinaryFormatter bf = new BinaryFormatter(); StreamWriter sw = new StreamWriter("test.txt"); bf.Serialize(sw.BaseStream, new A() { SomeString = "abc", SomeValue = 123 }); sw.Close();
As you can see, I passed a new instance of class A containing abc and 123 as values.
Examples of result data:
If we look at the serialized result in a hex editor, we get something like this:

Let's interpret the data from the example result:
According to the above specification (here is a direct link to the PDF: [MS-NRBF] .pdf ) each record within the stream is identified using RecordTypeEnumeration . Section 2.1.2.1 RecordTypeNumeration states:
This enumeration identifies the type of record. Each entry (except MemberPrimitiveUnTyped) begins with an enumeration of the entry type. The listing size is one BYTE.
SerializationHeaderRecord:
So, if we look back at the data received, we can begin to interpret the first byte:

As indicated in 2.1.2.1 RecordTypeEnumeration , a value of 0 identifies the SerializationHeaderRecord specified in 2.6.1 SerializationHeaderRecord :
The SerializationHeaderRecord entry MUST be the first entry in binary serialization. This entry has a major and minor version of the format and identifiers of the top object and headers.
It consists of:
- RecordTypeEnum (1 byte)
- RootId (4 bytes)
- HeaderId (4 bytes)
- MajorVersion (4 bytes)
- MinorVersion (4 bytes)
With this knowledge, we can interpret a record containing 17 bytes:

00 represents RecordTypeEnumeration , which is SerializationHeaderRecord in our case.
01 00 00 00 presents RootId
If neither BinaryMethodCall nor BinaryMethodReturn is present in the serialization stream, the value of this field MUST contain the ObjectId of the Class, Array, or BinaryObjectString record contained in the serialization stream.
So, in our case, it should be an ObjectId with a value of 1 (because the data is serialized using little-endian), which we hope to see again; -)
FF FF FF FF Presents HeaderId
01 00 00 00 Presents MajorVersion
00 00 00 00 presents MinorVersion
in the BinaryLibrary:
As indicated, each record should begin with RecordTypeEnumeration . At the end of the last recording, we must assume that a new one begins.
Let's interpret the following byte:

As we can see, in our SerializationHeaderRecord example, the BinaryLibrary entry BinaryLibrary :
The BinaryLibrary entry associates the INT32 identifier (as described in [2.2.22] MS-DTYP] with the library name. This allows other entries to reference the library name using the identifier. This approach reduces wire size when there are multiple entries that reference the same and same library name.
It consists of:
- RecordTypeEnum (1 byte)
- LibraryId (4 bytes)
- LibraryName (variable number of bytes (
LengthPrefixedString ))
As stated in 2.1.1.6 LengthPrefixedString ...
LengthPrefixedString is a string value. The string has a UTF-8 encoded string length prefix in bytes. The length is encoded in a variable-length field with a minimum of 1 byte and no more than 5 bytes. To minimize wire size, the length is encoded as a variable length field.
In our simple example, the length is always encoded using 1 byte . With this knowledge, we can continue to interpret the bytes in the stream:

0C represents a RecordTypeEnumeration that identifies a BinaryLibrary record.
02 00 00 00 represents LibraryId , which is 2 in our case.
Now LengthPrefixedString follows:

42 represents LengthPrefixedString information that contains a LibraryName .
In our case, information about the length of 42 (decimal 66) tells us that we need to read the next 66 bytes and interpret them as LibraryName .
As already mentioned, the UTF-8 string is encoded, so the result of the bytes above will be something like this: _WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
ClassWithMembersAndTypes:
And the recording is complete again, so we interpret the RecordTypeEnumeration following:

05 identifies the ClassWithMembersAndTypes entry. Section 2.3.2.1 ClassWithMembersAndTypes states:
The ClassWithMembersAndTypes entry is the most verbose of the Class entries. It contains metadata about members, including the names and types of deleted items. It also contains a library identifier that references the name of the class library.
It consists of:
- RecordTypeEnum (1 byte)
- ClassInfo (variable number of bytes)
- MemberTypeInfo (variable number of bytes)
- LibraryId (4 bytes)
ClassInfo:
As stated in 2.3.1.1 ClassInfo , an entry consists of:
- ObjectId (4 bytes)
- Name (variable number of bytes (again,
LengthPrefixedString )) - MemberCount (4 bytes)
- MemberNames (which is a sequence of
LengthPrefixedString , where the number of elements MUST be equal to the value specified in the MemberCount field.)
Back to the original data, step by step:

01 00 00 00 represents the ObjectId . We already saw this, it was listed as RootId in SerializationHeaderRecord .

0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41 represents the Name class, which is represented using LengthPrefixedString . As already mentioned, in our example, the length of the string is determined with 1 byte, so the first byte 0F indicates that 15 bytes should be read and decoded using UTF-8. The result looks something like this: StackOverFlow.A - so I used StackOverFlow as the namespace name.

02 00 00 00 represents MemberCount , it tells us that 2 members will follow, both of which are represented by LengthPrefixedString .
First Member Name: 
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 represents the first MemberName , 1B is again a string length of 27 bytes, which leads to something like this : <SomeString>k__BackingField .
Second Member Name: <T411>
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 represents the second MemberName , 1A indicates that the string length is 26 bytes. This leads to something like this: <SomeValue>k__BackingField .
MemberTypeInfo:
ClassInfo followed by MemberTypeInfo .
Section 2.3.1.2 - MemberTypeInfo indicates that the structure contains:
- BinaryTypeEnums (variable in length)
A sequence of BinaryTypeEnumeration values ββthat represents the passed member types. Array MUST:
Have the same number of elements as the MemberNames field of the ClassInfo structure.
We will arrange so that the BinaryTypeEnumeration matches the name of the member in the MemberNames field of the ClassInfo structure.
- Additionally
BinaryTpeEnum (variable in length), depending on BinaryTpeEnum additional information may or may not be.
| BinaryTypeEnum | AdditionalInfos |
|----------------+--------------------------|
| Primitive | PrimitiveTypeEnumeration |
| String | None |
Therefore, given this, we are almost there ... We expect 2 BinaryTypeEnumeration values ββ(because MemberNames had 2 members).
MemberTypeInfo go back to the source data of the full MemberTypeInfo record:

01 represents the BinaryTypeEnumeration first member, according to 2.1.2.2 BinaryTypeEnumeration can be expected a String , and it is represented using the LengthPrefixedString .
00 represents the BinaryTypeEnumeration second element, and, again, according to the specification, this is Primitive . As stated above, Primitive followed by additional information, in this case a PrimitiveTypeEnumeration . Therefore, we need to read the next byte, which is 08 , compare it with the table specified in 2.1.2.3 PrimitiveTypeEnumeration , and be surprised that we can expect Int32 , which is represented by 4 bytes, as indicated in some other document on basic data types.
LibraryId:
After MemerTypeInfo follows LibraryId , it is represented by 4 bytes:

02 00 00 00 represents LibraryId , which is 2.
Values:
As stated in 2.3 Class Records :
The values ββof class members MUST be serialized as records that follow this record, as described in section 2.7. The order of entries MUST match the order of MemberNames, as specified in the ClassInfo structure (section 2.3.1.1).
That is why we can now expect member values.
Let's look at the last few bytes:

06 identifies a BinaryObjectString . It represents the value of our SomeString property ( <SomeString>k__BackingField , to be precise).
According to 2.5.7 BinaryObjectString it contains:
- RecordTypeEnum (1 byte)
- ObjectId (4 bytes)
- Value (variable length represented as
LengthPrefixedString )
Therefore, knowing this, we can clearly determine that

03 00 00 00 represents the ObjectId .
03 61 62 63 represents Value , where 03 is the length of the string itself, and 61 62 63 are the bytes of the content, which translate to abc .
I hope you remember that there was a second member, Int32 . Knowing that Int32 is represented using 4 bytes, we can conclude that

must be the Value our second member. 7B hexadecimal equivalent of 123 decimal characters, which apparently matches our example.
So here is the complete ClassWithMembersAndTypes entry: 
MessageEnd:

Finally, the last byte 0B represents the MessageEnd record.