Files
DarkflameServer/dCommon/AMFDeserialize.cpp
Aaron Kimbrell 8a2ccb1ae7 fix: bound AMF3 decoding and stop hashing client keys
Client-sent AMF (CONTROL_BEHAVIORS) was decoded without limits on the
associative part of arrays, on nesting depth, or on the total number of
values. Associative entries went into an unordered_map with the standard
unseeded string hash, so a client could pick colliding keys and make
insertion quadratic, and deeply nested arrays recursed until the stack
overflowed, crashing the world server.

- The associative map is now an ordered std::map (O(log n) whatever the
  keys, deterministic serialization order).
- Each array allows at most 10,000 associative entries, the same as the
  existing dense limit, which is now checked before anything is read.
- Arrays may nest at most 32 deep and one deserializer reads at most
  100,000 values. Every limit throws, which the only caller already
  catches and drops the message.
- Inserting a duplicate key keeps the last value and no longer returns a
  reference to a value that was destroyed when the key already held null.

Verified with new unit tests for each limit (including a 100,000 deep
nesting that previously overflowed the stack) and the existing live
packet test, which still decodes.

Fixes #2035

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:44 -05:00

167 lines
5.5 KiB
C++

#include "AMFDeserialize.h"
#include <stdexcept>
#include "Amf3.h"
#include "StringifiedEnum.h"
/**
* AMF3 Reference document https://rtmp.veriskope.com/pdf/amf3-file-format-spec.pdf
* AMF3 Deserializer written by EmosewaMC
*/
std::unique_ptr<AMFBaseValue> AMFDeserialize::Read(RakNet::BitStream& inStream) {
// Every value counts against the budget for this deserializer, so one message
// cannot make the server build an unbounded tree of values.
if (++m_ValuesRead > MaxValues) {
LOG("AMF value budget of %u exceeded, possible spoof, aborting deserialize.", MaxValues);
throw std::invalid_argument("AMF value budget exceeded");
}
// Read in the value type from the bitStream
eAmf marker;
inStream.Read(marker);
// Based on the typing, create the value associated with that and return the base value class
switch (marker) {
case eAmf::Undefined:
return std::make_unique<AMFBaseValue>();
case eAmf::Null:
return std::make_unique<AMFNullValue>();
case eAmf::False:
return std::make_unique<AMFBoolValue>(false);
case eAmf::True:
return std::make_unique<AMFBoolValue>(true);
case eAmf::Integer:
return ReadAmfInteger(inStream);
case eAmf::Double:
return ReadAmfDouble(inStream);
case eAmf::String:
return ReadAmfString(inStream);
case eAmf::Array:
return ReadAmfArray(inStream);
// These values are unimplemented in the live client and will remain unimplemented
// unless someone modifies the client to allow serializing of these values.
case eAmf::XMLDoc:
[[fallthrough]];
case eAmf::Date:
[[fallthrough]];
case eAmf::Object:
[[fallthrough]];
case eAmf::XML:
[[fallthrough]];
case eAmf::ByteArray:
[[fallthrough]];
case eAmf::VectorInt:
[[fallthrough]];
case eAmf::VectorUInt:
[[fallthrough]];
case eAmf::VectorDouble:
[[fallthrough]];
case eAmf::VectorObject:
[[fallthrough]];
case eAmf::Dictionary:
throw std::invalid_argument(StringifiedEnum::ToString(marker).data());
default:
throw std::invalid_argument("Invalid AMF3 marker" + std::to_string(static_cast<int32_t>(marker)));
}
}
uint32_t AMFDeserialize::ReadU29(RakNet::BitStream& inStream) {
bool byteFlag = true;
uint32_t actualNumber{};
uint8_t numberOfBytesRead{};
while (byteFlag && numberOfBytesRead < 4) {
uint8_t byte{};
inStream.Read(byte);
// Parse the byte
if (numberOfBytesRead < 3) {
byteFlag = byte & static_cast<uint8_t>(1 << 7);
byte = byte << 1UL;
}
// Combine the read byte with our current read in number
actualNumber <<= 8UL;
actualNumber |= static_cast<uint32_t>(byte);
// If we are not done reading in bytes, shift right 1 bit
if (numberOfBytesRead < 3) actualNumber = actualNumber >> 1UL;
numberOfBytesRead++;
}
return actualNumber;
}
const std::string AMFDeserialize::ReadString(RakNet::BitStream& inStream) {
auto length = ReadU29(inStream);
// Check if this is a reference
bool isReference = length % 2 == 1;
// Right shift by 1 bit to get index if reference or size of next string if value
length = length >> 1;
if (isReference) {
constexpr int32_t maxStringSize = 1024 * 1024;
if (length > maxStringSize) {
LOG("1MB string attempted to be allocated in AMF deserialize, possible spoof, aborting deserialize.");
throw std::invalid_argument("1MB string attempted to be allocated in AMF deserialize, possible spoof, aborting deserialize.");
}
std::string value(length, 0);
inStream.Read(&value[0], length);
// Empty strings are never sent by reference
if (!value.empty()) accessedElements.push_back(value);
return value;
} else {
// Length is a reference to a previous index - use that as the read in value
return accessedElements.at(length);
}
}
std::unique_ptr<AMFDoubleValue> AMFDeserialize::ReadAmfDouble(RakNet::BitStream& inStream) {
double value;
inStream.Read<double>(value);
return std::make_unique<AMFDoubleValue>(value);
}
std::unique_ptr<AMFArrayValue> AMFDeserialize::ReadAmfArray(RakNet::BitStream& inStream) {
// Arrays are the only values that nest, so bound the recursion here.
if (m_Depth >= MaxDepth) {
LOG("AMF arrays nested deeper than %u, possible spoof, aborting deserialize.", MaxDepth);
throw std::invalid_argument("AMF arrays nested too deeply");
}
++m_Depth;
auto arrayValue = std::make_unique<AMFArrayValue>();
// Read size of dense array
const auto sizeOfDenseArray = (ReadU29(inStream) >> 1);
if (sizeOfDenseArray > MaxArraySize) {
LOG("Someone sent %u dense array entries, probably a bad packet.", sizeOfDenseArray);
throw std::invalid_argument("Too many dense AMF array entries");
}
// Then read associative portion
uint32_t associativeEntries = 0;
while (true) {
const auto key = ReadString(inStream);
// No more associative values when we encounter an empty string key
if (key.size() == 0) break;
if (++associativeEntries > MaxArraySize) {
LOG("Someone sent more than %u associative array entries, probably a bad packet.", MaxArraySize);
throw std::invalid_argument("Too many associative AMF array entries");
}
arrayValue->Insert(key, Read(inStream));
}
// Finally read dense portion
for (uint32_t i = 0; i < sizeOfDenseArray; i++) {
arrayValue->Insert(i, Read(inStream));
}
--m_Depth;
return arrayValue;
}
std::unique_ptr<AMFStringValue> AMFDeserialize::ReadAmfString(RakNet::BitStream& inStream) {
return std::make_unique<AMFStringValue>(ReadString(inStream));
}
std::unique_ptr<AMFIntValue> AMFDeserialize::ReadAmfInteger(RakNet::BitStream& inStream) {
return std::make_unique<AMFIntValue>(ReadU29(inStream)); // NOTE: NARROWING CONVERSION FROM UINT TO INT. IS THIS INTENDED?
}