Ładunki odczytu BinaryFormatter (NRBF)
BinaryFormatter użyto .NET Remoting: format binarny do serializacji. Ten format jest znany ze skrótu MS-NRBF lub tylko NRBF. Typowym wyzwaniem podczas migracji z BinaryFormatter programu jest obsługa ładunków utrwalone w magazynie, ponieważ odczyt tych ładunków był wcześniej wymagany BinaryFormatter. Niektóre systemy muszą zachować możliwość odczytywania tych ładunków w celu stopniowego migracji do nowych serializatorów, unikając odwołania do BinaryFormatter samego siebie.
W ramach platformy .NET 9 wprowadzono nową klasę NrbfDecoder w celu dekodowania ładunków NRBF bez wykonywania deserializacji ładunku. Ten interfejs API można bezpiecznie użyć do dekodowania zaufanych lub niezaufanych ładunków bez ryzyka, które BinaryFormatter niesie deserializacja. Jednak NrbfDecoder jedynie dekoduje dane do struktur, które aplikacja może jeszcze bardziej przetworzyć. Podczas korzystania z narzędzia NrbfDecoder należy zachować ostrożność, aby bezpiecznie załadować dane do odpowiednich wystąpień.
Ostrożność
NrbfDecoder jest implementacją czytnika NRBF, ale jego zachowanie nie ściśle zgodne z implementacją BinaryFormatter. W związku z tym nie należy używać danych wyjściowych NrbfDecoder w celu określenia, czy wywołanie BinaryFormatter byłoby bezpieczne.
Można traktować NrbfDecoder jako odpowiednik użycia czytnika JSON/XML bez deserializacji.
NrbfDecoder
NrbfDecoder jest częścią nowego pakietu NuGet System.Formats.Nrbf . Jest ona przeznaczona nie tylko dla platformy .NET 9, ale także starszych elementów monikers, takich jak .NET Standard 2.0 i .NET Framework. Ta wielowersyjność umożliwia wszystkim, którzy używają obsługiwanej wersji platformy .NET do migracji z dala od BinaryFormatterprogramu . Program NrbfDecoder może odczytywać ładunki, które zostały serializowane przy BinaryFormatter użyciu ( FormatterTypeStyle.TypesAlways wartość domyślna).
NrbfDecoder jest przeznaczony do traktowania wszystkich danych wejściowych jako niezaufanych. W związku z tym mają następujące zasady:
- Brak jakiegokolwiek rodzaju ładowania typu (aby uniknąć ryzyka, takiego jak zdalne wykonywanie kodu).
- Brak rekursji jakiegokolwiek rodzaju (aby uniknąć rekursji niezwiązanej, przepełnienia stosu i odmowy usługi).
- Brak wstępnego alokacji buforu na podstawie rozmiaru podanego w ładunku, jeśli ładunek jest zbyt mały, aby zawierać obiecane dane (aby uniknąć braku pamięci i odmowy usługi).
- Zdekoduj każdą część danych wejściowych tylko raz (aby wykonać tę samą ilość pracy co potencjalny atakujący, który utworzył ładunek).
- Użyj losowego skrótu odpornego na kolizję, aby przechowywać rekordy, do których odwołują się inne rekordy (aby uniknąć braku pamięci dla słownika wspieranego przez tablicę, której rozmiar zależy od liczby kolizji kodu skrótu).
- Wystąpienie tylko typów pierwotnych można utworzyć w niejawny sposób. Tablice można utworzyć na żądanie. Inne typy nigdy nie są tworzone.
Ostrożność
W przypadku korzystania z nrbfDecoderważne jest, aby nie wprowadzać tych funkcji w kodzie ogólnego przeznaczenia, ponieważ spowoduje to negację tych zabezpieczeń.
Deserializowanie zamkniętego zestawu typów
NrbfDecoder jest przydatny tylko wtedy, gdy lista serializowanych typów jest znanym, zamkniętym zestawem. Aby umieścić go w inny sposób, musisz wiedzieć z góry, co chcesz odczytać, ponieważ musisz również utworzyć wystąpienia tych typów i wypełnić je danymi odczytanymi z ładunku. Rozważmy dwa przeciwległe przykłady:
- Wszystkie
[Serializable]
typy z Quartz.NET , które mogą być utrwalane przez samą bibliotekę, tosealed
. Nie ma więc typów niestandardowych, które użytkownicy mogą tworzyć, a ładunek może zawierać tylko znane typy. Typy udostępniają również konstruktory publiczne, więc można odtworzyć te typy na podstawie informacji odczytanych z ładunku. - Typ SettingsPropertyValue uwidacznia właściwość PropertyValue typu
object
, która może być używana BinaryFormatter wewnętrznie do serializacji i deserializacji dowolnego obiektu przechowywanego w pliku konfiguracji. Może służyć do przechowywania liczby całkowitej, typu niestandardowego, słownika lub dosłownie dowolnych elementów. Z tego powodu nie można migrować tej biblioteki bez wprowadzania zmian powodujących niezgodność w interfejsie API.
Identyfikowanie ładunków NRBF
NrbfDecoder udostępnia dwie StartsWithPayloadHeader metody umożliwiające sprawdzenie, czy dany strumień lub bufor zaczyna się od nagłówka NRBF. Zaleca się użycie tych metod podczas migrowania ładunków utrwałych BinaryFormatter do innego serializatora:
- Sprawdź, czy ładunek odczytany z magazynu jest ładunkiem NRBF za pomocą polecenia NrbfDecoder.StartsWithPayloadHeader.
- Jeśli tak, odczytaj go za pomocą NrbfDecoder.Decodepolecenia , serializuj go z powrotem przy użyciu nowego serializatora i zastąp dane w magazynie.
- Jeśli nie, użyj nowego serializatora, aby deserializować dane.
internal static T LoadFromFile<T>(string path)
{
bool update = false;
T value;
using (FileStream stream = File.OpenRead(path))
{
if (NrbfDecoder.StartsWithPayloadHeader(stream))
{
value = LoadLegacyValue<T>(stream);
update = true;
}
else
{
value = LoadNewValue<T>(stream);
}
}
if (update)
{
File.WriteAllBytes(path, NewSerializer(value));
}
return value;
}
Bezpieczne odczytywanie ładunków NRBF
Ładunek NRBF składa się z rekordów serializacji, które reprezentują serializowane obiekty i ich metadane. Aby odczytać cały ładunek i pobrać obiekt główny, należy wywołać metodę Decode .
Metoda Decode zwraca SerializationRecord wystąpienie. SerializationRecord jest klasą abstrakcyjną, która reprezentuje rekord serializacji i udostępnia trzy właściwości opisujące się samodzielnie: Id, RecordTypei TypeName.
Uwaga
Osoba atakująca może utworzyć ładunek z cyklami (na przykład: klasa lub tablica obiektów z odwołaniem do samego siebie). Id zwraca wystąpienie SerializationRecordId, które implementuje IEquatable<T> i między innymi, może służyć do wykrywania cykli w dekodowanych rekordach.
SerializationRecord udostępnia metodę TypeNameMatches, która porównuje nazwę typu odczytaną z ładunku (i uwidocznioną za pośrednictwem właściwości TypeName) z określonym typem. Ta metoda ignoruje nazwy zestawów, więc użytkownicy nie muszą martwić się o przekazywanie typów i przechowywanie wersji zestawu. Nie uwzględnia również nazw elementów członkowskich ani ich typów (ponieważ uzyskanie tych informacji wymaga ładowania typu).
using System.Formats.Nrbf;
static Animal Pseudocode(Stream payload)
{
SerializationRecord record = NrbfDecoder.Read(payload);
if (record.TypeNameMatches(typeof(Cat)) && record is ClassRecord catRecord)
{
return new Cat()
{
Name = catRecord.GetString("Name"),
WorshippersCount = catRecord.GetInt32("WorshippersCount")
};
}
else if (record.TypeNameMatches(typeof(Dog)) && record is ClassRecord dogRecord)
{
return new Dog()
{
Name = dogRecord.GetString("Name"),
FriendsCount = dogRecord.GetInt32("FriendsCount")
};
}
else
{
throw new Exception($"Unexpected record: `{record.TypeName.AssemblyQualifiedName}`.");
}
}
Istnieje ponad kilkanaście różnych typów rekordów serializacji . Ta biblioteka udostępnia zestaw abstrakcji, więc wystarczy nauczyć się tylko kilku z nich:
-
PrimitiveTypeRecord<T>: opisuje wszystkie typy pierwotne natywnie obsługiwane przez NRBF (
string
,bool
,byte
sbyte
char
short
ushort
int
uint
long
ulong
float
double
decimal
TimeSpan
i ).DateTime
- Uwidacznia wartość za pośrednictwem
Value
właściwości . -
PrimitiveTypeRecord<T> pochodzi z niegenerycznej PrimitiveTypeRecord, która również uwidacznia Value właściwość . Jednak w klasie bazowej wartość jest zwracana jako
object
(która wprowadza boxing dla typów wartości).
- Uwidacznia wartość za pośrednictwem
-
ClassRecord: opisuje wszystkie
class
istruct
oprócz wyżej wymienionych typów pierwotnych. - ArrayRecord: opisuje wszystkie rekordy tablic, w tym tablice postrzępione i wielowymiarowe.
-
SZArrayRecord<T>: opisuje jednowymiarowe, bezindeksowane rekordy tablicowe, w których
T
może być typem pierwotnym lub .SerializationRecord
SerializationRecord rootObject = NrbfDecoder.Decode(payload); // payload is a Stream
if (rootObject is PrimitiveTypeRecord primitiveRecord)
{
Console.WriteLine($"It was a primitive value: '{primitiveRecord.Value}'");
}
else if (rootObject is ClassRecord classRecord)
{
Console.WriteLine($"It was a class record of '{classRecord.TypeName.AssemblyQualifiedName}' type name.");
}
else if (rootObject is SZArrayRecord<byte> arrayOfBytes)
{
Console.WriteLine($"It was an array of `{arrayOfBytes.Length}`-many bytes.");
}
Obok Decodemetody , nrbfDecoder uwidacznia metodę DecodeClassRecord , która zwraca ClassRecord (lub zgłasza).
KlasaRecord
Najważniejszym typem pochodzącym z SerializationRecord klasy jest ClassRecord, który reprezentuje wszystkie class
struct
wystąpienia obok tablic i natywnie obsługiwanych typów pierwotnych. Umożliwia odczytywanie wszystkich nazw i wartości elementów członkowskich. Aby zrozumieć, czym jest element członkowski, zobacz dokumentację BinaryFormatterfunkcji.
Interfejs API, który udostępnia:
- MemberNames właściwość, która pobiera nazwy serializowanych elementów członkowskich.
- HasMember metoda sprawdzająca, czy element członkowski podanej nazwy znajdował się w ładunku. Został on zaprojektowany do obsługi scenariuszy przechowywania wersji, w których można było zmienić nazwę danego elementu członkowskiego.
- Zestaw dedykowanych metod pobierania wartości pierwotnych podanej nazwy składowej: GetString, , , GetBooleanGetByteGetSByteGetCharGetInt16GetUInt16GetInt32GetUInt32GetInt64GetUInt64GetSingleGetDoubleGetDecimalGetTimeSpani .GetDateTime
- GetClassRecord pobiera wystąpienie [ClassRecord]. W przypadku cyklu odnosi się to do tego samego wystąpienia bieżącego [ClassRecord] z tym samym numerem Id.
- GetArrayRecord pobiera instancję [ArrayRecord].
- GetSerializationRecord pobrać dowolny rekord serializacji i GetRawValue do pobrania dowolnego rekordu serializacji lub wartości pierwotnej.
Poniższy fragment kodu pokazuje ClassRecord działanie:
[Serializable]
public class Sample
{
public int Integer;
public string? Text;
public byte[]? ArrayOfBytes;
public Sample? ClassInstance;
}
ClassRecord rootRecord = NrbfDecoder.DecodeClassRecord(payload);
Sample output = new()
{
// using the dedicated methods to read primitive values
Integer = rootRecord.GetInt32(nameof(Sample.Integer)),
Text = rootRecord.GetString(nameof(Sample.Text)),
// using dedicated method to read an array of bytes
ArrayOfBytes = ((SZArrayRecord<byte>)rootRecord.GetArrayRecord(nameof(Sample.ArrayOfBytes))).GetArray(),
};
// using GetClassRecord to read a class record
ClassRecord? referenced = rootRecord.GetClassRecord(nameof(Sample.ClassInstance));
if (referenced is not null)
{
if (referenced.Id.Equals(rootRecord.Id))
{
throw new Exception("Unexpected cycle detected!");
}
output.ClassInstance = new()
{
Text = referenced.GetString(nameof(Sample.Text))
};
}
TablicaRekord
ArrayRecord definiuje podstawowe zachowanie rekordów tablic NRBF i udostępnia bazę dla klas pochodnych. Zapewnia dwie właściwości:
- Rank, który oblicza stopień tablicy.
- Lengths, który otrzymuje bufor liczb całkowitych reprezentujących liczbę elementów w każdym wymiarze. Zaleca się sprawdzenie całkowitej długości podanego rekordu tablicy przed wywołaniem GetArray.
Zapewnia również jedną metodę: GetArray. Gdy jest używany po raz pierwszy, przydziela tablicę i wypełnia ją danymi podanymi w serializowanych rekordach (w przypadku natywnie obsługiwanych typów pierwotnych, takich jak string
lub int
) lub serializacji rekordów (w przypadku tablic typów złożonych).
GetArray wymaga obowiązkowego argumentu, który określa typ oczekiwanej tablicy. Jeśli na przykład rekord powinien być tablicą liczb całkowitych 2D, expectedArrayType
element musi zostać podany jako typeof(int[,])
i zwracana tablica jest również int[,]
następująca:
ArrayRecord arrayRecord = (ArrayRecord)NrbfDecoder.Decode(stream);
if (arrayRecord.Rank != 2 || arrayRecord.Lengths[0] * arrayRecord.Lengths[1] > 10_000)
{
throw new Exception("The array had unexpected rank or length!");
}
int[,] array2d = (int[,])arrayRecord.GetArray(typeof(int[,]));
Jeśli występuje niezgodność typu (na przykład osoba atakująca dostarczyła ładunek z tablicą dwóch miliardów ciągów), metoda zgłasza błąd InvalidOperationException.
Ostrożność
Niestety format NRBF ułatwia osobie atakującej kompresowanie dużej liczby elementów tablicy o wartości null. Dlatego zaleca się zawsze sprawdzenie całkowitej długości tablicy przed wywołaniem GetArray. Ponadto GetArray akceptuje opcjonalny argument logiczny allowNulls
, który po ustawieniu wartości false
spowoduje zgłoszenie wartości null.
Funkcja NrbfDecoder nie ładuje ani nie tworzy wystąpienia żadnych typów niestandardowych, więc w przypadku tablic typów złożonych zwraca tablicę SerializationRecord.
[Serializable]
public class ComplexType3D
{
public int I, J, K;
}
ArrayRecord arrayRecord = (ArrayRecord)NrbfDecoder.Decode(payload);
if (arrayRecord.Rank != 1 || arrayRecord.Lengths[0] > 10_000)
{
throw new Exception("The array had unexpected rank or length!");
}
SerializationRecord[] records = (SerializationRecord[])arrayRecord.GetArray(expectedArrayType: typeof(ComplexType3D[]), allowNulls: false);
ComplexType3D[] output = records.OfType<ClassRecord>().Select(classRecord => new ComplexType3D()
{
I = classRecord.GetInt32(nameof(ComplexType3D.I)),
J = classRecord.GetInt32(nameof(ComplexType3D.J)),
K = classRecord.GetInt32(nameof(ComplexType3D.K)),
}).ToArray();
Program .NET Framework obsługiwał tablice indeksowane niezerowo w ładunkach NRBF, ale ta obsługa nigdy nie została przekierowana do platformy .NET (Core). W związku z tym nrbfDecoder nie obsługuje dekodowania niezerowych indeksowanych tablic.
SZArrayRecord
SZArrayRecord<T>
Definiuje podstawowe zachowanie dla jednowymiarowych, bezindeksowanych rekordów tablicy NRBF i zapewnia podstawę dla klas pochodnych. Może T
to być jeden z natywnie obsługiwanych typów pierwotnych lub SerializationRecord.
Zapewnia Length właściwość i GetArray przeciążenie, które zwraca wartość T[]
.
[Serializable]
public class PrimitiveArrayFields
{
public byte[]? Bytes;
public uint[]? UnsignedIntegers;
}
ClassRecord rootRecord = NrbfDecoder.DecodeClassRecord(payload);
SZArrayRecord<byte> bytes = (SZArrayRecord<byte>)rootRecord.GetArrayRecord(nameof(PrimitiveArrayFields.Bytes));
SZArrayRecord<uint> uints = (SZArrayRecord<uint>)rootRecord.GetArrayRecord(nameof(PrimitiveArrayFields.UnsignedIntegers));
if (bytes.Length > 100_000 || uints.Length > 100_000)
{
throw new Exception("The array exceeded our limit");
}
PrimitiveArrayFields output = new()
{
Bytes = bytes.GetArray(),
UnsignedIntegers = uints.GetArray()
};