• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Протокол своими руками. Создаем с нуля TCP-протокол и пишем сервер на C#

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
Протокол своими руками. Создаем с нуля TCP-протокол и пишем сервер на C#

Ты в жизни не раз сталкивался с разными протоколами — одни использовал, другие, возможно, реверсил. Одни были легко читаемы, в других без hex-редактора не разобраться. В этой статье я покажу, как создать свой собственный протокол, который будет работать поверх TCP/IP. Мы разработаем свою структуру данных и реализуем сервер на C#.
Итак, протокол передачи данных — это соглашение между приложениями о том, как должны выглядеть передаваемые данные. Например, сервер и клиент могут использовать WebSocketв связке с JSON. Вот так приложение на Android могло бы запросить погоду с сервера:
Код:
{
    "request": "getWeather",
    "city": "cityname"
}
И сервер мог бы ответить:
Код:
{
    "success": true,
    "weatherHumanReadable": "Warm",
    "degrees": 18
}
Пропарсив ответ по известной модели, приложение предоставит информацию пользователю. Выполнить парсинг такого пакета можно, только располагая информацией о его строении. Если ее нет, протокол придется реверсить.


Создаем базовую структуру протокола

Этот протокол будет базовым для простоты. Но мы будем вести его разработку с расчетом на то, что впоследствии его расширим и усложним.
Первое, что необходимо ввести, — это наш собственный заголовок, чтобы приложения могли отличать пакеты нашего протокола. У нас это будет набор байтов 0xAF, 0xAA, 0xAF. Именно они и будут стоять в начале каждого сообщения.

Почти каждый бинарный протокол имеет свое «магическое число» (также «заголовок» и «сигнатура») — набор байтов в начале пакета. Оно используется для идентификации пакетов своего протокола. Остальные пакеты будут игнорироваться.

Каждый пакет будет иметь тип и подтип и будет размером в байт. Так мы сможем создать 65 025 (255 * 255) разных типов пакетов. Пакет будет содержать в себе поля, каждое со своим уникальным номером, тоже размером в один байт. Это предоставит возможность иметь 255 полей в одном пакете. Чтобы удостовериться в том, что пакет дошел до приложения полностью (и для удобства парсинга), добавим байты, которые будут сигнализировать о конце пакета.
Завершенная структура пакета:
Код:
XPROTOCOL PACKET STRUCTURE

(offset: 0) HEADER (3 bytes) [ 0xAF, 0xAA, 0xAF ]
(offset: 3) PACKET ID
(offset: 3) PACKET TYPE (1 byte)
(offset: 4) PACKET SUBTYPE (1 byte)
(offset: 5) FIELDS (FIELD[])
(offset: END) PACKET ENDING (2 bytes) [ 0xFF, 0x00 ]

FIELD STRUCTURE

(offset: 0) FIELD ID (1 byte)
(offset: 1) FIELD SIZE (1 byte)
(offset: 2) FIELD CONTENTS
Назовем наш протокол, как ты мог заметить, XProtocol. На третьем сдвиге начинается информация о типе пакета. На пятом начинается массив из полей. Завершающим звеном будут байты 0xFF и 0x00, закрывающие пакет.


Пишем клиент и сервер

Для начала нужно ввести основные свойства, которые будет иметь пакет:
  • тип пакета;
  • подтип;
  • набор полей.
Код:
public class XPacket
{
    public byte PacketType { get; private set; }
    public byte PacketSubtype { get; private set; }
    public List<XPacketField> Fields { get; set; } = new List<XPacketField>();
}
Добавим класс для описания поля пакета, в котором будут его данные, ID и размер.
Код:
public class XPacketField
{
    public byte FieldID { get; set; }
    public byte FieldSize { get; set; }
    public byte[] Contents { get; set; }
}
Сделаем обычный конструктор приватным и создадим статический метод для получения нового экземпляра объекта.
Код:
private XPacket() {}

public static XPacket Create(byte type, byte subtype)
{
    return new XPacket
    {
        PacketType = type,
        PacketSubtype = subtype
    };
}
Теперь можно задать тип пакета и поля, которые будут внутри него. Создадим функцию для этого. Записывать будем в поток MemoryStream. Первым делом запишем байты заголовка, типа и подтипа пакета, а потом отсортируем поля по возрастанию FieldID.
Код:
public byte[] ToPacket()
{
    var packet = new MemoryStream();

    packet.Write(
    new byte[] {0xAF, 0xAA, 0xAF, PacketType, PacketSubtype}, 0, 5);

    var fields = Fields.OrderBy(field => field.FieldID);

    foreach (var field in fields)
    {
        packet.Write(new[] {field.FieldID, field.FieldSize}, 0, 2);
        packet.Write(field.Contents, 0, field.Contents.Length);
    }

    packet.Write(new byte[] {0xFF, 0x00}, 0, 2);

    return packet.ToArray();
}
Теперь запишем все поля. Сначала пойдет ID поля, его размер и данные. И только потом конец пакета — 0xFF, 0x00.

Теперь пора научиться парсить пакеты. Минимальный размер пакета — 7 байт: HEADER(3) + TYPE(1) + SUBTYPE(1) + PACKET ENDING(2)

Проверяем размер входного пакета, его заголовок и два последних байта. После валидации пакета получим его тип и подтип.
Код:
public static XPacket Parse(byte[] packet)
{
    if (packet.Length < 7)
    {
        return null;
    }

    if (packet[0] != 0xAF ||
        packet[1] != 0xAA ||
        packet[2] != 0xAF)
    {
        return null;
    }

    var mIndex = packet.Length - 1;

    if (packet[mIndex - 1] != 0xFF ||
        packet[mIndex] != 0x00)
    {
        return null;
    }

    var type = packet[3];
    var subtype = packet[4];

    var xpacket = Create(type, subtype);

    /* <---> */
Пора перейти к парсингу полей. Так как наш пакет заканчивается двумя байтами, мы можем узнать, когда закончились данные для парсинга. Получим ID поля и его размер, добавим к списку. Если пакет будет поврежден и будет существовать поле с ID, равным нулю, и SIZE, равным нулю, то необходимости его парсить нет.
Код:
    /* <---> */

    var fields = packet.Skip(5).ToArray();

    while (true)
    {
        if (fields.Length == 2)
        {
            return xpacket;
        }

        var id = fields[0];
        var size = fields[1];

        var contents = size != 0 ?
        fields.Skip(2).Take(size).ToArray() : null;

        xpacket.Fields.Add(new XPacketField
        {
            FieldID = id,
            FieldSize = size,
            Contents = contents
        });

        fields = fields.Skip(2 + size).ToArray();
    }
}
У кода выше есть проблема: если подменить размер одного из полей, парсинг завершится с необработанным исключением или пропарсит пакет неверно. Необходимо обеспечить безопасность пакетов. Но об этом речь пойдет чуть позже.


Учимся записывать и считывать данные

Из-за строения класса XPacket необходимо хранить бинарные данные для полей. Чтобы установить значение поля, нам потребуется конвертировать имеющиеся данные в массив байтов. Язык C# не предоставляет идеальных способов сделать это, поэтому внутри пакетов будут передаваться только базовые типы: int, double, float и так далее. Так как они имеют фиксированный размер, можно считать его напрямую из памяти.
Чтобы получить чистые байты объекта из памяти, иногда используется метод небезопасного кода и указателей, но есть и способы проще: благодаря классу Marshal в C# можно взаимодействовать с unmanaged-областями нашего приложения. Чтобы перевести любой объект фиксированной длины в байты, мы будем пользоваться такой функцией:
Код:
public byte[] FixedObjectToByteArray(object value)
{
    var rawsize = Marshal.SizeOf(value);
    var rawdata = new byte[rawsize];

    var handle = GCHandle.Alloc(rawdata,
        GCHandleType.Pinned);

    Marshal.StructureToPtr(value,
        handle.AddrOfPinnedObject(),
        false);

    handle.Free();

    return rawdata;
}
Здесь мы делаем следующее:
  • получаем размер нашего объекта;
  • создаем массив, в который будет записана вся информация;
  • получаем дескриптор на наш массив и записываем в него объект.
Теперь сделаем то же самое, только наоборот.
Код:
private T ByteArrayToFixedObject<T>(byte[] bytes) where T: struct 
{
    T structure;

    var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);

    try
    {
        structure = (T) Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
    }
    finally
    {
        handle.Free();
    }

    return structure;
}
Только что ты научился превращать объекты в массив байтов и обратно. Сейчас можно добавить функции для установки и получения значений полей. Давай создадим функцию для простого поиска поля по его ID.
Код:
public XPacketField GetField(byte id)
{
    foreach (var field in Fields)
    {
        if (field.FieldID == id)
        {
            return field;
        }
    }

    return null;
}
Добавим функцию для проверки существования поля.
Код:
public bool HasField(byte id)
{
    return GetField(id) != null;
}
Получаем значение из поля.
Код:
public T GetValue<T>(byte id) where T : struct
{
    var field = GetField(id);

    if (field == null)
    {
        throw new Exception($"Field with ID {id} wasn't found.");
    }
    var neededSize = Marshal.SizeOf(typeof(T));

    if (field.FieldSize != neededSize)
    {
        throw new Exception($"Can't convert field to type {typeof(T).FullName}.\n" + $"We have {field.FieldSize} bytes but we need exactly {neededSize}.");
    }

    return ByteArrayToFixedObject<T>(field.Contents);
}
Добавив несколько проверок и используя уже известную нам функцию, превратим набор байтов из поля в нужный нам объект типа T.


Установка значения

Мы можем принять только объекты Value-Type. Они имеют фиксированный размер, поэтому мы можем их записать.
Код:
public void SetValue(byte id, object structure)
{
    if (!structure.GetType().IsValueType)
    {
        throw new Exception("Only value types are available.");
    }

    var field = GetField(id);

    if (field == null)
    {
        field = new XPacketField
        {
            FieldID = id
        };

        Fields.Add(field);
    }

    var bytes = FixedObjectToByteArray(structure);

    if (bytes.Length > byte.MaxValue)
    {
        throw new Exception("Object is too big. Max length is 255 bytes.");
    }

    field.FieldSize = (byte) bytes.Length;
    field.Contents = bytes;
}


Проверка на работоспособность

Проверим создание пакета, его перевод в бинарный вид и парсинг назад.
Код:
var packet = XPacket.Create(1, 0);

packet.SetValue(0, 123);
packet.SetValue(1, 123D);
packet.SetValue(2, 123F);
packet.SetValue(3, false);

var packetBytes = packet.ToPacket();
var parsedPacket = XPacket.Parse(packetBytes);

Console.WriteLine($"int: {parsedPacket.GetValue<int>(0)}\n" +
                  $"double: {parsedPacket.GetValue<double>(1)}\n" +
                  $"float: {parsedPacket.GetValue<float>(2)}\n" +
                  $"bool: {parsedPacket.GetValue<bool>(3)}");
Судя по всему, все работает прекрасно. В консоли должен появиться выхлоп.
Код:
int: 123
double: 123
float: 123
bool: False


Вводим типы пакетов

Запомнить ID всех пакетов, которые будут созданы, сложно. Отлаживать пакет с типом N и подтипом Ns не легче, если не держать все ID в голове. В этом разделе мы дадим нашим пакетам имена и привяжем эти имена к ID пакета. Для начала создадим перечисление, которое будет содержать имена пакетов.
Код:
public enum XPacketType
{
    Unknown,
    Handshake
}
Unknown будет использоваться для типа, который нам неизвестен. Handshake — для пакета рукопожатия.
Теперь, когда нам известны типы пакетов, пора привязать их к ID. Необходимо создать менеджер, который будет этим заниматься.
Код:
public static class XPacketTypeManager
{
    private static readonly Dictionary<XPacketType, Tuple<byte, byte>> TypeDictionary = new Dictionary<XPacketType, Tuple<byte, byte>>();
    /* < ... > */
}
Статический класс хорошо подойдет для этой функции. Его конструктор вызывается лишь один раз, что позволит нам зарегистрировать все известные типы пакетов. Невозможность вызвать статический конструктор извне поможет не проходить повторную регистрацию типов.
Dictionary<TKey, TValue> хорошо подходит для этой задачи. Используем тип (XPacketType) как ключ, а Tuple<T1, T2> будет хранить в себе значение типа (T1) и подтипа (T2). Создадим функцию для регистрации типов пакета.
Код:
public static void RegisterType(XPacketType type, byte btype, byte bsubtype)
{
    if (TypeDictionary.ContainsKey(type))
    {
        throw new Exception($"Packet type {type:G} is already registered.");
    }

    TypeDictionary.Add(type, Tuple.Create(btype, bsubtype));
}
Имплементируем получение информации по типу:
Код:
public static Tuple<byte, byte> GetType(XPacketType type)
{
    if (!TypeDictionary.ContainsKey(type))
    {
        throw new Exception($"Packet type {type:G} is not registered.");
    }

    return TypeDictionary[type];
}
И конечно, получение типа пакета. Структура может выглядеть несколько хаотичной, но она будет работать.
Код:
public static XPacketType GetTypeFromPacket(XPacket packet)
{
    var type = packet.PacketType;
    var subtype = packet.PacketSubtype;

    foreach (var tuple in TypeDictionary)
    {
        var value = tuple.Value;

        if (value.Item1 == type && value.Item2 == subtype)
        {
            return tuple.Key;
        }
    }

    return XPacketType.Unknown;
}


Создаем структуру пакетов для их сериализации и десериализации

Чтобы не парсить все вручную, обратимся к сериализации и десериализации классов. Для этого нужно создать класс и расставить атрибуты. Все остальное код сделает самостоятельно; потребуется только атрибут с информацией о том, с какого поля писать и читать.
Код:
[AttributeUsage(AttributeTargets.Field)]
public class XFieldAttribute : Attribute
{
    public byte FieldID { get; }

    public XFieldAttribute(byte fieldId)
    {
        FieldID = fieldId;
    }
}
Используя AttributeUsage, мы установили, что наш атрибут можно будет установить только на поля классов. FieldID будет использоваться для хранения ID поля внутри пакета.


Создаем сериализатор

Для сериализации и десериализации в C# используется Reflection. Этот набор классов позволит узнать всю необходимую информацию и установить значение полей во время рантайма.
Для начала необходимо собрать информацию о полях, которые будут участвовать в процессе сериализации. Для этого можно использовать простое выражение LINQ.
Код:
private static List<Tuple<FieldInfo, byte>> GetFields(Type t)
{
    return t.GetFields(BindingFlags.Instance |
                       BindingFlags.NonPublic |
                       BindingFlags.Public)
    .Where(field => field.GetCustomAttribute<XFieldAttribute>() != null)
    .Select(field => Tuple.Create(field, field.GetCustomAttribute<XFieldAttribute>().FieldID))
    .ToList();
}
Так как необходимые поля помечены атрибутом XFieldAttribute, найти их внутри класса не составит труда. Сначала получим все нестатичные, приватные и публичные поля при помощи GetFields(). Выбираем все поля, у которых есть наш атрибут. Собираем новый IEnumerable, который содержит Tuple<FieldInfo, byte>, где byte — ID нашего поля в пакете.

Здесь мы вызываем GetCustomAttribute<>() два раза. Это не обязательно, но таким образом код будет выглядеть аккуратнее.

Итак, теперь ты умеешь получать все FieldInfo для типа, который будешь сериализовать. Пришло время создать сам сериализатор: у него будут обычный и строгий режимы работы. Во время обычного режима будет игнорироваться тот факт, что разные поля используют один и тот же ID поля внутри пакета.
Код:
public static XPacket Serialize(byte type, byte subtype, object obj, bool strict = false)
{
    var fields = GetFields(obj.GetType());

    if (strict)
    {
        var usedUp = new List<byte>();

        foreach (var field in fields)
        {
            if (usedUp.Contains(field.Item2))
            {
                throw new Exception("One field used two times.");
            }

            usedUp.Add(field.Item2);
        }
    }

    var packet = XPacket.Create(type, subtype);

    foreach (var field in fields)
    {
        packet.SetValue(field.Item2, field.Item1.GetValue(obj));
    }

    return packet;
}
Внутри foreach происходит самое интересное: fields содержит все нужные поля в виде Tuple<FieldInfo, byte>. Item1 — искомое поле, Item2 — ID этого поля внутри пакета. Перебираем их все, следом устанавливаем значения полей при помощи SetPacket(byte, object). Теперь пакет сериализован.


Создаем десериализатор

Создавать десериализатор в разы проще. Нужно использовать функцию GetFields(), которую мы имплементировали в прошлом разделе.
Код:
public static T Deserialize<T>(XPacket packet, bool strict = false)
{
    var fields = GetFields(typeof(T));
    var instance = Activator.CreateInstance<T>();

    if (fields.Count == 0)
    {
        return instance;
    }

    /* <---> */
После того как мы подготовили все к десериализации, можем приступить к делу. Выполняем проверки для режима strict, бросая исключение, когда это нужно.
Код:
    /* <---> */

    foreach (var tuple in fields)
    {
        var field = tuple.Item1;
        var packetFieldId = tuple.Item2;

        if (!packet.HasField(packetFieldId))
        {
            if (strict)
            {
                throw new Exception($"Couldn't get field[{packetFieldId}] for {field.Name}");
            }

            continue;
        }

        /* Очень важный костыль, который многое упрощает
         * Метод GetValue<T>(byte) принимает тип как type-параметр
* Наш же тип внутри field.FieldType
* Используя Reflection, вызываем метод с нужным type-параметром
         */ 

        var value = typeof(XPacket)
            .GetMethod("GetValue")?
            .MakeGenericMethod(field.FieldType)
            .Invoke(packet, new object[] {packetFieldId});

        if (value == null)
        {
            if (strict)
            {
                throw new Exception($"Couldn't get value for field[{packetFieldId}] for {field.Name}");
            }

            continue;
        }

        field.SetValue(instance, value);
    }

    return instance;
}
Создание десериализатора завершено. Теперь можно проверить работоспособность кода. Для начала создадим простой класс.
Код:
class TestPacket
{
    [XField(0)]
    public int TestNumber;

    [XField(1)]
    public double TestDouble;

    [XField(2)]
    public bool TestBoolean;
}
Напишем простой тест.
Код:
var t = new TestPacket {TestNumber = 12345,
                        TestDouble = 123.45D,
                        TestBoolean = true};

var packet = XPacketConverter.Serialize(0, 0, t);
var tDes = XPacketConverter.Deserialize<TestPacket>(packet);

if (tDes.TestBoolean)
{
    Console.WriteLine($"Number = {tDes.TestNumber}\n" +
                      $"Double = {tDes.TestDouble}");
}
После запуска программы должны отобразиться две строки:
Код:
Number = 12345
Double = 123,45
А теперь перейдем к тому, для чего все это создавалось.


Первое рукопожатие

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

Примеры работы с сокетами ты найдешь в официальной документации в главе Socket Code Examples.

Мы создали простой пакет для обмена рукопожатиями.
Код:
public class XPacketHandshake
{
    [XField(1)]
    public int MagicHandshakeNumber;
}
Рукопожатие будет инициировать клиент. Он отправляет пакет рукопожатия с рандомным числом, а сервер в свою очередь должен ответить числом, на 15 меньше полученного.
Отправляем пакет на сервер.
Код:
var rand = new Random();
HandshakeMagic = rand.Next();

client.QueuePacketSend(
        XPacketConverter.Serialize(
            XPacketType.Handshake,
            new XPacketHandshake
            {
                MagicHandshakeNumber = HandshakeMagic
            }).ToPacket());
При получении пакета от сервера обрабатываем handshake отдельной функцией.
Код:
private static void ProcessIncomingPacket(XPacket packet)
{
    var type = XPacketTypeManager.GetTypeFromPacket(packet);

    switch (type)
    {
        case XPacketType.Handshake:
            ProcessHandshake(packet);
            break;
        case XPacketType.Unknown:
            break;
        default:
            throw new ArgumentOutOfRangeException();
    }
}
Десериализуем, проверяем ответ от сервера.
Код:
private static void ProcessHandshake(XPacket packet)
{
    var handshake = XPacketConverter.Deserialize<XPacketHandshake>(packet);

    if (HandshakeMagic - handshake.MagicHandshakeNumber == 15)
    {
        Console.WriteLine("Handshake successful!");
    }
}
На стороне сервера есть свой идентичный ProcessIncomingPacket. Разберем процесс обработки пакета на стороне сервера. Десериализуем пакет рукопожатия от клиента, отнимаем пятнадцать, сериализуем и отправляем обратно.
Код:
private void ProcessHandshake(XPacket packet)
{
    Console.WriteLine("Recieved handshake packet.");

    var handshake = XPacketConverter.Deserialize<XPacketHandshake>(packet);
    handshake.MagicHandshakeNumber -= 15;

    Console.WriteLine("Answering..");

    QueuePacketSend(
        XPacketConverter.Serialize(XPacketType.Handshake, handshake)
            .ToPacket());
}
Собираем и проверяем.
handshake-test.png

Тестирование рукопожатия

Все работает!


Имплементируем простую защиту протокола

Наш протокол будет иметь два типа пакетов — обычный и защищенный. У обычного наш стандартный заголовок, а у защищенного вот такой: [0x95, 0xAA, 0xFF].
Чтобы отличать зашифрованные пакеты от обычных, потребуется добавить свойство внутрь класса XPacket.
Код:
public bool Protected { get; set; }
После модифицируем функцию XPacket.Parse(byte[]), чтобы она принимала и расшифровывала новые пакеты. Сначала модифицируем функцию проверки заголовка:
Код:
var encrypted = false;

if (packet[0] != 0xAF ||
    packet[1] != 0xAA ||
    packet[2] != 0xAF)
{
    if (packet[0] == 0x95 ||
        packet[1] == 0xAA ||
        packet[2] == 0xFF)
    {
        encrypted = true;
    }
    else
    {
        return null;
    }
}
Как будет выглядеть наш зашифрованный пакет? По сути, это будет пакет в пакете (вроде пакета с пакетами, который ты прячешь на кухне, только здесь защищенный пакет содержит в себе зашифрованный обычный пакет).
Теперь необходимо расшифровать и распарсить зашифрованный пакет. Позволяем пометить пакет как продукт расшифровки другого пакета.
Код:
public static XPacket Parse(byte[] packet, bool markAsEncrypted = false)
Добавляем функциональность в цикл парсинга полей.
Код:
if (fields.Length == 2)
{
    return encrypted ? DecryptPacket(xpacket) : xpacket;
}
Так как мы принимаем только структуры как типы данных, мы не сможем записать byte[]внутрь поля. Поэтому немного модифицируем код, добавив новую функцию, которая будет принимать массив данных.
Код:
public void SetValueRaw(byte id, byte[] rawData)
{
    var field = GetField(id);

    if (field == null)
    {
        field = new XPacketField
        {
            FieldID = id
        };

        Fields.Add(field);
    }

    if (rawData.Length > byte.MaxValue)
    {
        throw new Exception("Object is too big. Max length is 255 bytes.");
    }

    field.FieldSize = (byte) rawData.Length;
    field.Contents = rawData;
}
Сделаем такую же, но уже для получения данных из поля.
Код:
public byte[] GetValueRaw(byte id)
{
    var field = GetField(id);

    if (field == null)
    {
        throw new Exception($"Field with ID {id} wasn't found.");
    }

    return field.Contents;
}
Теперь все готово для создания функции расшифровки пакета. Шифрование будет использовать класс RijndaelManaged со строкой в качестве пароля для шифрования. Строка с паролем будет константна. Это шифрование поможет защититься от атаки типа MITM.
Создадим класс, который будет шифровать и расшифровывать данные.

Так как процесс шифрования выглядит идентично, возьмем готовое решение для шифрования строки с Stack Overflow и адаптируем его для себя.

Модифицируем методы, чтобы они принимали и возвращали массивы байтов.
Код:
public static byte[] Encrypt(byte[] data, string passPhrase)
public static byte[] Decrypt(byte[] data, string passPhrase)
И простой хендлер, который будет хранить секретный ключ.
Код:
public class XProtocolEncryptor
{
    private static string Key { get; } = "2e985f930853919313c96d001cb5701f";

    public static byte[] Encrypt(byte[] data)
    {
        return RijndaelHandler.Encrypt(data, Key);
    }

    public static byte[] Decrypt(byte[] data)
    {
        return RijndaelHandler.Decrypt(data, Key);
    }
}
Затем создаем функцию для расшифровки. Данные обязательно должны быть в поле с ID = 0. Как иначе нам его искать?
Код:
private static XPacket DecryptPacket(XPacket packet)
{
    if (!packet.HasField(0))
    {
        return null;
    }

    var rawData = packet.GetValueRaw(0);
    var decrypted = XProtocolEncryptor.Decrypt(rawData);

    return Parse(decrypted, true);
}
Получаем данные, расшифровываем и парсим заново. То же самое проделываем с обратной процедурой.
Вводим свойство, чтобы пометить надобность в заголовке зашифрованного пакета.
Код:
private bool ChangeHeaders { get; set; }
Создаем простой пакет и помечаем, что в нем зашифрованные данные.
Код:
public static XPacket EncryptPacket(XPacket packet)
{
    if (packet == null)
    {
        return null;
    }

    var rawBytes = packet.ToPacket();
    var encrypted = XProtocolEncryptor.Encrypt(rawBytes);

    var p = Create(0, 0);
    p.SetValueRaw(0, encrypted);
    p.ChangeHeaders = true;

    return p;
}
И добавляем две функции для более удобного обращения.
Код:
public XPacket Encrypt()
{
    return EncryptPacket(this);
}

public XPacket Decrypt() {
    return DecryptPacket(this);
}
Модифицируем ToPacket(), чтобы тот слушался значения ChangeHeaders.
Код:
packet.Write(ChangeHeaders
    ? new byte[] {0x95, 0xAA, 0xFF, PacketType, PacketSubtype}
    : new byte[] {0xAF, 0xAA, 0xAF, PacketType, PacketSubtype},
    0, 5);
Проверяем:
Код:
var packet = XPacket.Create(0, 0);
packet.SetValue(0, 12345);

var encr = packet.Encrypt().ToPacket();
var decr = XPacket.Parse(encr);

Console.WriteLine(decr.GetValue<int>(0));
В консоли получаем число 12345.


Заключение

Только что мы создали свой собственный протокол. Это был долгий путь от базовой структуры на бумаге до его полной имплементации в коде. Надеюсь, тебе было интересно!
Исходный код проекта можно найти в моем GitHub.


(с) 0x25CBFC4F
хакер.ру
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх