Серийный класс .NET для переполнения стека

Я получаю некоторые сериализованные строковые данные класса .NET из источника, и мне просто нужно превратить их в нечто читаемое в PHP. Не обязательно превращать его в «объект» или JSON, но мне нужно как-то его прочитать. Я думаю, что строка .NET — это просто класс с некоторыми заданными свойствами, но он двоичный и, очевидно, не переносимый. Я не собираюсь конвертировать .NET-код в PHP-код. Вот пример данных:

U:?�S�@��-��v�Y��?������An�@AMAUI������

Я понимаю, что это на самом деле бинарный и не для печати текст. Я просто использую это как пример того, что я вижу при загрузке файла.

3

Решение

Короткий ответ:

Я бы действительно предложил НЕ реализовывать интерпретацию двоичного представления самостоятельно. Я бы использовал другой формат вместо (JSON, XML, так далее.).

Длинный ответ:

Однако, если это невозможно, есть, конечно, способ …

Актуальный вопрос: Как выглядит двоичный формат сериализованных объектов .NET и как мы можем правильно его интерпретировать?

Я основал все свои исследования на .NET Remoting: структура данных в двоичном формате Спецификация.

Пример класса:

Чтобы иметь рабочий пример, я создал простой класс с именем A который содержит 2 свойства, одну строку и одно целое значение, они называются SomeString а также SomeValue,

Учебный класс 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 содержащий abc а также 123 как значения.

Пример результатов данных:

Если мы посмотрим на сериализованный результат в шестнадцатеричном редакторе, мы получим что-то вроде этого:

Пример результатов

Давайте интерпретируем данные результата примера:

Согласно вышеупомянутой спецификации (вот прямая ссылка на PDF: [МС-NRBF] .pdf) каждая запись в потоке идентифицируется RecordTypeEnumeration, Раздел 2.1.2.1 RecordTypeNumeration состояния:

Это перечисление идентифицирует тип записи. Каждая запись (за исключением MemberPrimitiveUnTyped) начинается с перечисления типа записи. Размер перечисления составляет один байт.

SerializationHeaderRecord:

Поэтому, если мы оглянемся на полученные данные, мы можем начать интерпретацию первого байта:

SerializationHeaderRecord_RecordTypeEnumeration

Как указано в 2.1.2.1 RecordTypeEnumeration значение 0 идентифицирует SerializationHeaderRecord который указан в 2.6.1 SerializationHeaderRecord:

Запись SerializationHeaderRecord ДОЛЖНА быть первой записью в двоичной сериализации. Эта запись имеет основной и вспомогательный вариант формата, а также идентификаторы верхнего объекта и заголовков.

Это состоит из:

  • RecordTypeEnum (1 байт)
  • RootId (4 байта)
  • HeaderId (4 байта)
  • MajorVersion (4 байта)
  • MinorVersion (4 байта)

С этим знанием мы можем интерпретировать запись, содержащую 17 байтов:

SerializationHeaderRecord_Complete

00 представляет RecordTypeEnumeration который SerializationHeaderRecord в нашем случае.

01 00 00 00 представляет RootId

Если в потоке сериализации нет ни записи BinaryMethodCall, ни BinaryMethodReturn, значение этого поля ДОЛЖНО содержать ObjectId записи Class, Array или BinaryObjectString, содержащейся в потоке сериализации.

Так что в нашем случае это должно быть ObjectId со значением 1 (потому что данные сериализуются с использованием порядка байтов), который мы надеемся увидеть снова 😉

FF FF FF FF представляет HeaderId

01 00 00 00 представляет MajorVersion

00 00 00 00 представляет MinorVersion

BinaryLibrary:

Как указано, каждая запись должна начинаться с RecordTypeEnumeration, Поскольку последняя запись завершена, мы должны предположить, что начинается новая.

Давайте интерпретируем следующий байт:

BinaryLibraryRecord_RecordTypeEnumeration

Как мы видим, в нашем примере SerializationHeaderRecord это сопровождается BinaryLibrary запись:

Запись BinaryLibrary связывает идентификатор INT32 (как указано в разделе 2.2.22 [MS-DTYP]) с именем библиотеки. Это позволяет другим записям ссылаться на имя библиотеки с помощью идентификатора. Этот подход уменьшает размер провода, когда существует несколько записей, которые ссылаются на одно и то же имя библиотеки.

Это состоит из:

  • RecordTypeEnum (1 байт)
  • LibraryId (4 байта)
  • LibraryName (переменное число байтов (которое является LengthPrefixedString))

Как указано в 2.1.1.6 LengthPrefixedString

LengthPrefixedString представляет строковое значение. Строка начинается с длины строки в кодировке UTF-8 в байтах. Длина кодируется в поле переменной длины с минимальным 1 байтом и максимальным 5 байтами. Чтобы минимизировать размер провода, длина кодируется как поле переменной длины.

В нашем простом примере длина всегда кодируется с использованием 1 byte, С этим знанием мы можем продолжить интерпретацию байтов в потоке:

BinaryLibraryRecord_RecordTypeEnumeration_LibraryId

0C представляет RecordTypeEnumeration который идентифицирует BinaryLibrary запись.

02 00 00 00 представляет LibraryId который 2 в нашем случае.

Теперь LengthPrefixedString следующим образом:

BinaryLibraryRecord_RecordTypeEnumeration_LibraryId_LibraryName

42 представляет информацию о длине LengthPrefixedString который содержит LibraryName,

В нашем случае длина информации 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 является наиболее подробной из записей класса. Он содержит метаданные об Участниках, включая имена и Типы Удаленного взаимодействия Участников. Он также содержит идентификатор библиотеки, который ссылается на имя библиотеки класса.

Это состоит из:

  • RecordTypeEnum (1 байт)
  • ClassInfo (переменное число байтов)
  • MemberTypeInfo (переменное число байтов)
  • LibraryId (4 байта)

ClassInfo:

Как указано в 2.3.1.1 ClassInfo запись состоит из:

  • ObjectId (4 байта)
  • Имя (переменное число байтов (которое снова LengthPrefixedString))
  • MemberCount (4 байта)
  • MemberNames (который представляет собой последовательность LengthPrefixedStringгде количество элементов ДОЛЖНО быть равно значению, указанному в MemberCount поле.)

Вернуться к необработанным данным, шаг за шагом:

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId

01 00 00 00 представляет ObjectId, Мы уже видели этот, он был указан как RootId в SerializationHeaderRecord,

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name

0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41 представляет Name класса, который представлен с помощью LengthPrefixedString, Как уже упоминалось, в нашем примере длина строки определяется 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 представляет первый MemberName, 1B снова длина строки, которая составляет 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, представляющая типы элементов, которые передаются. Массив ДОЛЖЕН:

  • Содержать то же количество элементов, что и поле MemberNames структуры ClassInfo.

  • Упорядочить так, чтобы BinaryTypeEnumeration соответствовал имени члена в поле MemberNames структуры ClassInfo.

  • AdditionalInfos (переменной длины), в зависимости от BinaryTpeEnum дополнительная информация может или не может присутствовать.

| BinaryTypeEnum | AdditionalInfos |
|----------------+--------------------------|
| Primitive | PrimitiveTypeEnumeration |
| String | None |

Итак, учитывая это, мы почти на месте …
Мы ожидаем 2 BinaryTypeEnumeration значения (потому что у нас было 2 члена в MemberNames).

Опять же, вернемся к необработанным данным полного MemberTypeInfo запись:

ClassWithMembersAndTypesRecord_MemberTypeInfo

01 представляет BinaryTypeEnumeration первого члена, согласно 2.1.2.2 BinaryTypeEnumeration мы можем ожидать String и он представлен с использованием LengthPrefixedString,

00 представляет BinaryTypeEnumeration второго члена, и опять же, согласно спецификации, это Primitive, Как указано выше, Primitiveсопровождаются дополнительной информацией, в данном случае PrimitiveTypeEnumeration, Вот почему нам нужно прочитать следующий байт, который 08сопоставьте его с таблицей, указанной в 2.1.2.3 PrimitiveTypeEnumeration и с удивлением заметил, что мы можем ожидать Int32 который представлен 4 байтами, как указано в каком-то другом документе об основных типах данных.

LibraryId:

После MemerTypeInfo LibraryId следует, это представлено 4 байтами:

ClassWithMembersAndTypesRecord_LibraryId

02 00 00 00 представляет LibraryId который 2.

Ценности:

Как указано в 2.3 Class Records:

Значения членов класса ДОЛЖНЫ быть сериализованы как записи, которые следуют за этой записью, как указано в разделе 2.7. Порядок записей ДОЛЖЕН соответствовать порядку MemberNames, указанному в структуре ClassInfo (раздел 2.3.1.1).

Вот почему мы можем теперь ожидать значения членов.

Давайте посмотрим на последние несколько байтов:

BinaryObjectStringRecord_RecordTypeEnumeration

06 идентифицирует BinaryObjectString, Это представляет ценность нашего SomeString собственность ( <SomeString>k__BackingField если быть точным).

В соответствии с 2.5.7 BinaryObjectString это содержит:

  • RecordTypeEnum (1 байт)
  • ObjectId (4 байта)
  • Значение (переменная длина, представленная в виде 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 запись.

6

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]