Методы защиты от внеигровых ботов

Мы разработали бота для NetChess. Это простое приложение для игры в шахматы по локальной сети. Современные онлайн-игры насчитывают тысячи пользователей, которые подключаются к серверу через Интернет. Несмотря на эти различия, разработка внеигровых ботов в обоих случаях пойдёт по одному и тому же плану. Прежде всего необходимо изучить протокол взаимодействия игрового клиента и сервера.

У приложения NetChess нет никакой защиты от реверс-инжиниринга и внеигровых ботов. Именно по этой причине нам так быстро удалось понять его протокол. Если вы попробуете проделать то же самое с современной онлайн-игрой, возникнут сложности. Скорее всего, вы не сможете так просто установить соответствие между действиями игрока и данными в перехваченных пакетах. Одни и те же действия могут менять байты по разным смещениям без какой-либо закономерности. Если вы столкнулись с подобным поведением, значит игра имеет систему защиты. Самый надёжный и распространённый подход для защиты трафика приложения – это шифрование.

В главе 3 мы применяли алгоритмы шифрования для защиты памяти приложения. Теперь рассмотрим, как с их помощью обезопасить сетевой трафик.

Криптосистема

Перед изучением практических примеров, рассмотрим понятие криптосистемы. Криптосистема – это набор криптографических алгоритмов для обеспечения конфиденциальности информации. Как правило, она предоставляет алгоритмы для следующих целей:

  1. Генерация ключа.

  2. Шифрование.

  3. Дешифрование.

Первая категория алгоритмов в списке используется для создания [секретного ключа](https://ru.wikipedia.org/wiki/Ключ_(криптография)), который удовлетворяет требованиям шифра.

Как работает шифрование? Предположим, что у нас есть некоторая информация (например сообщение), которое мы хотим защитить от несанкционированного чтения. Эта информация называется открытый текст (plaintext). Она вместе с секретным ключом передаётся алгоритму шифрования. После отработки алгоритм выдаст информацию в зашифрованном виде, который называется шифротекст. Чтобы снова получить открытый текст, необходимо передать шифротекст и ключ в алгоритм дешифрования. Это значит, что исходное сообщение смогут прочитать только те получатели, которые знают ключ.

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

Тестовое приложение

Для демонстрации алгоритмов шифрования воспользуемся простым приложением, которое передаёт текстовое сообщение по протоколу UDP. Мы использовали это приложение в разделе "Перехват трафика" (см. листинги 4-3 и 4-4). Немного изменим скрипт отправителя, чтобы вместо трёх байт отправлялась строка "Hello world!".

{caption: "Листинг 4-7. Скрипт TestStringUdpSender.py", format: Python} `TestStringUdpSender.py`

Скрипт отправляет строку, хранящуюся в переменной data. Это байтовый массив, в котором каждой букве соответствует один байт (кодировка ASCII). Чтобы получить этот массив из исходной строки в кодировке UTF-8, используется функция bytes.

Запустите скрипт TestUdpReceiver.py из листинга 4-3 и TestStringUdpSender.py. Когда получатели примет сообщение, он выведет на консоль текст:

b'Hello world!'

Символ "b" в начале строки означает, что строка хранится в памяти в виде байтового массива.

Иллюстрация 4-22 демонстрирует перехваченный пакет тестового приложения.

{caption: "Иллюстрация 4-22. Перехваченный пакет тестового приложения"} Перехваченный пакет

Wireshark корректно декодировал строку "Hello world!". Мы можем её прочитать в нижней части окна анализатора в области байтового представления пакета.

Шифр XOR

Шифр XOR представляет собой одну из простейших криптосистем. Мы использовали его в главе 3 для сокрытия данных процесса от сканеров памяти. Теперь применим его для шифрования сетевого пакета.

Библиотека PyCrypto предоставляет реализацию шифра XOR. Мы воспользуемся ею вместо того, чтобы писать алгоритм самостоятельно.

W> В библиотеке PyCryptodome нет реализации шифра XOR. Если вы установили её, а не PyCrypto, вы не сможете запустить примеры из листингов 4-8, 4-9, 4-10 и 4-11.

Листинг 4-8 демонстрирует использование шифра XOR, предоставляемого библиотекой PyCrypto.

{caption: "Листинг 4-8. Скрипт XorTest.py", format: Python} `XorTest.py`

Первая строка скрипта импортирует Python модуль XOR, в котором реализованы алгоритмы шифра. Чтобы ими воспользоваться, нам надо подготовить секретный ключ. Им служит строка "The secret key", хранящаяся в переменной key.

Чтобы зашифровать строку, мы создаём объект encryption_suite класса XORCipher с помощью функции new (вызов XOR.new). В качестве параметра передаём в неё секретный ключ. У созданного объекта есть метод encrypt, который применяет шифр к переданному ему открытому тексту в формате байтового массива. Получившийся шифротекст сохраняется в переменной cipher_text и выводится на консоль. Он выглядит следующим образом:

b'\x1c\r\tL\x1cE\x14\x1d\x17\x18DJ'

Оставшаяся часть функции main дешифрует шифротекст в исходный вид. Для этого мы создаём объект dencryption_suite точно так же, как и encryption_suite ранее. С помощью метода decrypt этого объекта мы дешифруем строку, хранящуюся в переменной cipher_text, и выводим результат на консоль. Он должен совпасть с исходной строкой "Hello world!".

После внимательного изучения кода листинга 4-8 возникает вопрос. Можно ли использовать один и тот же объект класса XORCipher для шифрования и дешифрования? Ответ – нет. Классы библиотеки PyCrypto имеют внутреннее состояние, которое зависит от последней операции, выполненной с их помощью. Это означает, что любое действие над ними окажет влияние на последующее. Если вы зашифруете две строки друг за другом с помощью одного объекта, расшифровать их возможно только в той же последовательности. Иначе результат будет ошибочным. Надёжный и правильный способ использовать объекты XORCipher – использовать их для однократных операций шифрования и дешифрования.

Теперь применим шифр XOR для скриптов отправки и получения UDP-пакета нашего тестового приложения. Листинг 4-9 демонстрирует дополненный скрипт отправителя.

{caption: "Листинг 4-9. Скрипт XorUdpSender.py", format: Python} `XorUdpSender.py`

В скрипте XorUdpSender.py мы шифруем строку "Hello world!" и отправляем её по протоколу UDP.

Скрипт получателя приведён в листинге 4-10.

{caption: "Листинг 4-10. Скрипт XorUdpReceiver.py", format: Python} `XorUdpReceiver.py`

Если вы запустите скрипты отправителя и получателя, результат будет тем же что и раньше. Скрипт XorUdpReceiver.py выведет на консоль полученную строку:

b'Hello world!'

Однако, если вы перехватите передаваемый пакет с помощью Wireshark, вы сразу заметите разницу. Этот пакет приведён на иллюстрации 4-23.

{caption: "Иллюстрация 4-23. Перехваченный пакет, который был зашифрован XOR"} Пакет зашифрованный XOR

Обратите внимание, что теперь Wireshark не может декодировать строку. Вы можете сделать это вручную, но только если вам известен секретный ключ.

Возможно, некоторые читатели решат, что шифр XOR – это отличный вариант для защиты приложения. Он прост в использовании и быстро работает. На самом деле его очень легко взломать. Рассмотрим подробнее, как это сделать.

В шифре применяется логическая операция исключающее "или". Предположим, что мы шифруем открытый текст A с помощью секретного ключа K. Тогда получим шифротекст B:

A ⊕ K = B

Если мы применим исключающее "или" к A и B, то получим ключ K:

A ⊕ B = K

Это означает, что можно восстановить секретный ключ, если известны открытый текст и шифротекст. Скрипт XorCrack.py из листинга 4-11 восстанавливает ключ по рассмотренному алгоритму.

{caption: "Листинг 4-11. Скрипт XorCrack.py", format: Python} `XorCrack.py`

При запуске этот скрипт выведет на консоль следующее:

b'\x1c\r\tL\x1cE\x14\x1d\x17\x18DJ'
b'Hello world!'
b'The secret k'

Первая строка соответствует шифротексту. Далее идёт открытый текст и восстановленный секретный ключ.

Почему скрипт XorCrack.py восстановил только часть секретного ключа? В XOR шифре оператор исключающего "или" последовательно применяется к каждой букве открытого текста и соответствующему ей байту ключа. Если ключ оказался короче текста, оставшаяся его часть не используется. В противном случае он будет применяться циклично.

Как рассмотренное свойство оператора исключающего "или" поможет нам расшифровать пакет с реальными игровыми данными? В этом случае у нас есть только шифротекст. Наша задача – получить из него открытый текст. Прежде всего, необходимо восстановить секретный ключ. Для этого возьмём известный нам открытый текст и зашифруем его в точности той же криптосистемой, которой пользуется игровое приложение. Получив шифротекст, мы применим операцию исключающего "или" к нему и открытому тексту. Так мы узнаем ключ.

Например, мы заполняем форму регистрации для онлайн-игры. В ней надо указать информацию о новом игроке (имя, пароль, адрес электронной почты). Все эти данные нам известны. После заполнения формы, обычно требуется нажать кнопку "отправить". Перехватим пакеты, которые игровое приложение посылает по этому нажатию. В них передаются данные пользователя из формы регистрации. Применим оператор исключающего "или" к введённой нами информации об игроке и шифротексту из пакета. Чтобы перепробовать все комбинации, понадобится время, но рано или поздно мы восстановим секретный ключ.

Можно заключить, что у шифра XOR есть положительные стороны, но он не способен обеспечить надёжную защиту для трафика приложения.

Шифр Triple DES

Следующий шифр, который мы рассмотрим, называется Triple DES (3DES). Для шифрования в нём троекратно применяется алгоритм DES (Data Encryption Standard), который был разработан в 1975 году компанией IBM. Сегодня DES считается ненадёжным из-за использования коротких секретных ключей длиной 56 бит. Современные компьютеры позволяют перебрать все возможные ключи такой длины (количеством 2^56^) в течение нескольких дней. Алгоритм 3DES решает эту проблему путём увеличения длины ключа в три раза до 168 бит.

Почему необходимо применять алгоритм DES именно три раза? Разве не хватит двух? В этом случае мы получили бы ключ длиной 112 бит, которого достаточно для современных требований надёжности. Ожидается, что для взлома шифра потребуется перебрать 2^112^ всех возможных комбинаций. К сожалению, это предположение неверно. Атака под названием встреча посередине (meet-in-the-middle) позволяет сократить число вариантов ключей для перебора до 2^57^. Этого недостаточно для надёжного шифрования открытого текста. Если же применить алгоритм 3DES, атакующему (лицу взламывающему шифр) придётся перебрать 2^112^ комбинаций ключей, даже если он применит атаку встреча посередине.

При разработке шифра 3DES учитывалось, насколько удобно будет его применение на специальных чипах. Сегодня по-прежнему эксплуатируется много устройств, выпущенных десять и более лет назад. Они поддерживают алгоритм DES на аппаратном уровне. Эти устройства достаточно просто настроить на работу с шифром 3DES. Обратная совместимость с устаревшими решениями – это основная причина использования 3DES в наши дни. Более современные шифры быстрее и надёжнее.

Обе библиотеки PyCrypto и PyCryptodome предоставляют реализации шифров DES и 3DES. Мы рассмотрим только 3DES алгоритм.

Листинг 4-12 демонстрирует скрипт для шифрования и дешифрования строки с помощью 3DES.

{caption: "Листинг 4-12. Скрипт 3DesTest.py", format: Python} `3DesTest.py`

В этом скрипте мы импортируем Python модули DES3 и Random библиотеки PyCrypto. Первый из них предоставляет класс DES3Cipher, в котором реализованы алгоритмы шифрования и дешифрования. Модуль Random предоставляет генератор случайных последовательностей байтов. Его следует использовать вместо стандартного модуля random, распространяемого с интерпретатором Python. Потому что random считается небезопасным для целей шифрования.

Зачем алгоритму 3DES понадобился массив случайных байтов? 3DES – это блочный шифр. В нём открытый текст разделяется на блоки, которые последовательно шифруются с помощью секретного ключа. Если мы применим алгоритм как есть, шифр будет недостаточно надёжным. Причина в том, что атакующий может найти закономерность между отдельными блоками открытого текста и шифротекста. Тогда он сможет определить или по крайней мере предположить содержимое зашифрованных блоков. Чтобы предотвратить эту уязвимость, надо смешать каждый блок открытого текста с предыдущим блоком шифротекста. Этот подход известен как сцепление блоков шифротекста (Cipher Block Chaining или CBC). Единственная проблема возникает с первым блоком открытого текста. С какими данными следует смешивать его? Решение заключается в использовании случайно сгенерированный данных. Они называются вектором инициализации (Initialization Vector или IV).

В скрипте 3DesTest.py мы создаём файлоподобный объект (file-like) с помощью функции new модуля Random. После этого вызываем метод read, который возвращает массив случайных байтов указанной длины. Она должна быть равна длине одного блока, на которые разбивается открытый текст в алгоритме 3DES. В нашем случае это константа реализации DES3.block_size, равная восьми байтам. Мы сохраняем массив случайных байтов в переменной iv. Он будет смешиваться с первым блоком открытого текста при шифровании.

Возможно, вы заметили, что мы расширили секретный ключ двумя дополнительными символами до 16 байт. При использовании алгоритма 3DES длина ключа может быть либо 16, либо 24 байта.

После подготовки вектора инициализации и ключа, мы создаём объект encryption_suite класса DES3Cipher с помощью функции new модуля DES3. Она принимает три входных параметра:

  1. Секретный ключ.

  2. Режим сцепления блоков шифротекста.

  3. Вектор инициализации (если он нужен для выбранного режима).

В скрипте 3DesTest.py используется режим DES3.MODE_CBC. Библиотека PyCrypto предоставляет несколько альтернативных вариантов. Вы можете выбрать один из них.

W> При конструировании объектов для шифрования и дешифрования необходимо указывать один и тот же режим сцепления блоков шифротекста и вектор инициализации.

Интерфейс методов encrypt и decrypt класса DES3Cipher такой же, как и у XORCipher. Первый принимает на вход открытый текст, а второй – шифротекст.

После запуска скрипта 3DesTest.py, вывод на консоли должен выглядеть следующим образом:

b'\xdc\xce\xf1^_\x95[\x16K\x93\x9a\xb8\x01\xf3\x1b\xcb'
b'Hello world!    '

Обратите внимание, что мы добавили четыре пробела в конце строки открытого текста "Hello world!". Они необходимы, поскольку его длина должна быть кратна восьми байтам, т.е. длине блока шифрования. Это требование выбранного нами режима сцепления блоков шифротекста.

Теперь дополним скрипты отправки и приёма UDP сообщения так, чтобы они использовали 3DES шифр. Листинг 4-13 демонстрирует код отправителя.

{caption: "Листинг 4-13. Скрипт 3DesUdpSender.py", format: Python} `3DesUdpSender.py`

Скрипт 3DesUdpSender.py шифрует открытый текст так же, как и 3DesTest.py. Единственное отличие в том, что мы добавляем вектор инициализации в начало шифротекста. Затем отправляем его получателю в UDP-пакете. Для чего это нужно? Как вы помните, для дешифровки сообщения нужен секретный ключ и вектор инициализации. Ключ мы можем сгенерировать заранее и сохранить на стороне отправителя и получателя. К сожалению, проделать то же самое с вектором инициализации не получится. Он должен быть уникальным для каждой операции шифрования, иначе алгоритм будет скомпрометирован и атакующему будет проще взломать шифр. Следовательно, получатель сообщения должен каким-то образом узнать IV. Самое простое решение – отправлять его вместе с шифротекстом в одном пакете.

Возникает вопрос: безопасно ли передавать вектор инициализации в открытом виде? Да, это вполне безопасно. Главная задача IV – добавлять случайность в шифротекст. Благодаря ему мы получаем разный результат при шифровании одного и того же открытого текста. При применении криптосистем IV часто рассматривается как обязательная часть шифротекста.

Листинг 4-14 демонстрирует реализацию скрипта получателя.

{caption: "Листинг 4-14. Скрипт 3DesUdpReceiver.py", format: Python} `3DesUdpReceiver.py`

В скрипте 3DesUdpReceiver.py мы передаём первый блок данных (байты с нулевого по DES3.block_size) из принятого UDP-пакета в функцию new в качестве вектора инициализации. Она конструирует объект decryption_suite, с помощью которого мы расшифровываем оставшиеся байты сообщения.

Если вы запустите сначала скрипт 3DesUdpReceiver.py, а потом 3DesUdpSender.py, получатель корректно расшифрует переданное сообщение и выведет его на консоль.

Вы можете использовать шифр 3DES в своих приложениях только тогда, когда на это есть серьёзные причины (например аппаратная поддержка со стороны используемого оборудования). Сегодня он не считается достаточно надёжным. Теоретические варианты атаки на шифр рассмотрены в этой статье. Кроме того, современные шифры работают быстрее 3DES.

Шифр AES

В 1998 году два бельгийских криптографа Винсент Рэймен и Йоан Даймен создали шифр AES (Advanced Encryption Standard). Он заменил DES и его вариации в качестве криптографического стандарта США.

В AES были решены проблемы шифра DES. Прежде всего он позволяет использовать длинные секретные ключи: 128, 192 и 256 бит. Любой из вариантов не вызовет накладных расходов алгоритма шифрования, как в случае 3DES. Их отсутствие – одна из причин высокой скорости работы AES. Возможность выбора появилась потому, что в AES длины блоков и ключа могут различаться.

Обе библиотеки PyCrypto и PyCryptodome предоставляют шифр AES. Интерфейс для его использования похож на 3DES.

Листинг 4-15 демонстрирует применение AES для шифрования и дешифрования строки.

{caption: "Листинг 4-15. Скрипт AesTest.py", format: Python} `AesTest.py`

Сравните скрипты AesTest.py и 3DesTest.py. Они очень похожи. Функция new модуля AES создаёт объект encryption_suite класса AESCipher. У неё те же три входных параметра, что и в случае 3DES: секретный ключ, режим сцепления блоков, IV. Кроме того, AES поддерживает те же режимы сцепления, что и 3DES.

После запуска скрипта AesTest.py, в консоли напечатаются следующие строки:

b'\xed\xd5\x19]\x04\xba\xc5\x05^s\x18t\xa3\xb59x'
b'Hello world!    '

Нам опять пришлось дополнить открытый текст пробелами, до длины кратной восьми байтов. Это требование режима сцепления блоков AES.MODE_CBC.

Листинг 4-16 демонстрирует скрипт AesUdpSender.py, который шифрует сообщение алгоритмом AES и отправляет его.

{caption: "Листинг 4-16. Скрипт AesUdpSender.py", format: Python} `AesUdpSender.py`

Здесь мы отправляем IV в начале данных пакета точно так же, как и в скрипте 3DesUdpSender.py (листинг 4-13). Алгоритм шифрования и отправки пакета такой же, как при использовании 3DES.

Скрипт AesUdpReceiver.py из листинга 4-17 получает и дешифрует сообщение.

Скрипт AesUdpReceiver.py работает по тому же алгоритму, что и 3DesUdpReceiver.py из листинга 4-14.

Попробуйте запустить скрипты отправителя и получателя, чтобы проверить корректность их работы.

Если вы собираетесь использовать симметричный шифр в своём приложении, всегда выбирайте AES вместо 3DES.

Предположим, что игровое приложение, для которого мы собираемся написать бота, применяет симметричный шифр для защиты своего сетевого трафика. Можем ли мы его взломать, чтобы изучить протокол игры? Если используются надёжные алгоритмы вроде 3DES или AES, скорее всего, придётся перебрать и проверить все возможные комбинации секретных ключей. Этот подход известен как метод "грубой силы". Однако, существуют атаки на шифр, позволяющие уменьшить число ключей для перебора и проверки. Они специфичны для алгоритма шифрования, режима его работы, деталей реализации и качества выбранного секретного ключа.

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

Первое решение этого затруднения заключается в том, чтобы собрать информацию об открытом тексте. Мы можем прочитать состояния игровых объектов в окне приложения или проанализировать память его процесса. Высока вероятность, что эти состояния окажутся в одном из пакетов, которыми обменивается игровой клиент и сервер. Следовательно, если секретный ключ подойдёт, мы должны прочитать эти данные.

Альтернативное решение заключается в применении статистического теста к расшифрованным данным. Если проверяемый секретный ключ корректен, они должны быть более упорядоченными. Иначе мы получим набор случайных байтов без какой-либо закономерности.

Шифр RSA

Все рассмотренные нами ранее шифры (XOR, 3DES, AES) являются симметричными. Это означает, что для шифрования и дешифрования используется один и тот же секретный ключ. Следовательно, он должен быть у отправителя и получателя сообщения. Этот факт может навести на мысль: зачем вообще нужно взламывать надёжный шифр? Ведь по сути секретный ключ находится на стороне пользователя в памяти игрового клиента. Достаточно найти его и импортировать в код внеигрового бота. После этого он сможет взаимодействовать с сервером точно так же, как и оригинальный клиент.

Возникает встречный вопрос: возможно ли защитить секретный ключ на стороне игрового клиента? Лучшим решением было бы вообще не хранить его локально у пользователя. С другой стороны, сервер не может просто отправлять ключ перед началом каждого сеанса обмена пакетами. Если атакующий перехватит его, он легко расшифрует весь дальнейший трафик. Асимметричное шифрование решает именно эту проблему. Оно предоставляет алгоритмы для безопасной передачи ключа.

Рассмотрим асимметричный шифр RSA. Его идея заключается в том, чтобы применять одностороннюю математическую функцию для шифрования открытого текста. Ключ является входным параметром этой функции. Чтобы взломать шифр, необходимо решить математическое уравнение, то есть найти открытый текст по известному шифротексту и ключу. Однако, главная особенность односторонних функций заключается в сложности нахождения входного параметра по её известному значению. Поэтому взломать шифр за разумное время невозможно.

Если вычислить функцию обратную односторонней нельзя, как же тогда происходит дешифрование сообщения? Предположим, что мы зашифровали сообщение с помощью ключа и односторонней функции. Шифротекст передали получателю. Даже зная ключ, используемый при шифровании, он не сможет дешифровать сообщение. Нюанс заключается в том, что для асимметричного шифрования выбираются особенные односторонние функции: те у которых есть лазейка. Лазейка – эта некоторая подсказка, помогающая вычислить обратную функцию, т.е. получить открытый текст по известному шифротексту и ключу. Мы пришли к концепции двух ключей: первый для выполнения шифрования (известен как открытый ключ) и второй – лазейка для вычисления обратной функции (закрытый ключ).

Рассмотрим, как работает асимметричное шифрование с точки зрения пользователя. Наша задача – получить от другого лица зашифрованное сообщение. Сначала надо вычислить пару ключей: открытый и закрытый. Первый из них передаём отправителю информации. Он шифрует своё сообщение этим ключом и отправляет нам шифротекст. Благодаря закрытому ключу, который служит лазейкой к односторонней функции, мы расшифровываем сообщение. Как видно из рассмотренной схемы, атакующий может перехватить открытый ключ и шифротекст, но это не поможет ему расшифровать сообщение. Для этого нужен закрытый ключ, но его получатель хранит у себя и никому не передаёт.

Обе библиотеки PyCrypto и PyCryptodome предоставляют реализацию шифра RSA. Но в PyCryptodome отсутствуют некоторые недостаточно надёжные функции RSA.

Листинг 4-18 демонстрирует использование RSA для шифрования и дешифрования строки.

W> Скрипт RsaTest.py не заработает, если вы используете библиотеку PyCryptodome.

В скрипте мы импортируем два модуля Python Random и RSA. Первый из них нам уже известен. Второй предоставляет функции для генерации и применения открытого и закрытого ключа.

Сначала мы создаём объект key класса _RSAobj с помощью функции generate модуля RSA. Он содержит пару ключей (открытый и закрытый). Первый параметр функции обязательный. Он задаёт длину ключей (в нашем случае 1024 бита). Второй параметр опциональный. В нём передаётся функция генерации случайных чисел.

После создания объекта key мы вызываем его методы encrypt и decrypt для шифрования и дешифрования текста.

Может возникнуть вопрос: где применяются открытый и закрытый ключи в нашем примере? Ведь явно они нигде в коде не упоминаются. На самом деле шифрование и дешифрование происходит в одном и том же процессе, поэтому нет необходимости в передаче открытого ключа. Если же передача нужна, объект key предоставляет методы для экспорта и импорта ключей.

В листинге 4-18 мы рассмотрели использование шифра RSA самого по себе. В таком виде он уязвим для атаки на основе подобранного открытого текста (chosen-plaintext attack или CPA). Поэтому RSA всегда используют в комбинации со схемой дополнения OAEP (Optimal Asymmetric Encryption Padding), которая предотвращает эту уязвимость. Такая комбинация шифра и схемы дополнения известна как RSA-OAEP.

Листинг 4-19 демонстрирует использование RSA-OAEP алгоритма для шифрования строки.

Теперь для шифрования и дешифрования мы используем не key, а объекты класса PKCS1OAEP_Cipher из модуля PKCS1_OAEP. Он конструируется функцией new, которая принимает входным параметром объект класса _RSAobj (то есть ключи RSA). Для шифрования и дешифрования используются два разных OAEP-объекта: encryption_suite и decryption_suite.

Применим RSA-OAEP шифр для нашего тестового приложения, отправляющего UDP-пакет по сети. Прежде всего необходимо изменить его алгоритм. В случае симметричного шифрования он тривиален: зашифровать открытый текст, передать его в пакете, расшифровать на стороне получателя. При применении асимметричного шифра появляется дополнительный шаг: передача открытого ключа отправителю сообщения. Ведь с его помощью и будет происходить шифрование.

Рассмотрим пошагово новый алгоритм тестового приложения:

  1. Скрипт отправителя сообщения запускается первым. Он создаёт UDP-сокет и ожидает получения открытого ключа.

  2. Скрипт получателя запускается. Он создаёт UDP-сокет. Затем генерирует пару ключей.

  3. Получатель сообщения посылает свой открытый ключ.

  4. Отправитель читает ключ из пришедшего UDP-пакета и использует его для шифрования открытого текста по алгоритму RSA-OAEP.

  5. Отправитель посылает шифротекст с сообщением.

  6. Получатель принимает шифротекст и дешифрует его, используя свой закрытый ключ.

Листинг 4-20 демонстрирует скрипт, отправляющий сообщение.

В этом скрипте мы используем функцию importKey модуля RSA. Она конструирует объект класса _RSAobj, содержащий только открытый ключ. Этого объекта будет достаточно для шифрования, но не для дешифрования. На входе importKey принимает ключ в формате байтового массива, который мы получаем из UDP-пакета. Переменная key используется для конструирования объекта cipher класса PKCS1OAEP_Cipher. С его помощью мы шифруем сообщение и отправляем его получателю.

Скрипт, получающий сообщение, приведён в листинге 4-21.

Как мы рассмотрели ранее, в скрипте получателя сообщения появились дополнительные шаги для передачи открытого ключа. Мы генерируем пару ключей и сохраняем её в объекте key. Затем с помощью его метода publickey создаём временный объект класса _RSAobj, содержащий только открытый ключ. Его нужно представить в формате байтового массива, чтобы передать в UDP-пакете. Для этого вызываем метод exportKey временного объекта. Результат сохраняем в переменную public_key.

Метод exportKey есть у любого объекта класса _RSAobj. Что он экспортирует, если мы вызовем его для объекта key, содержащего и открытый ключ, и закрытый? В этом случае метод вернёт закрытый ключ. Это может быть полезно для сохранения его на жёстком диске и дальнейшего использования.

Открытый ключ мы посылаем в UDP-пакете без шифрования. Его перехват не поможет в атаке на шифр. После этого мы ждём пока отправитель получит ключ, зашифрует им сообщение и отправит его. Для дешифровки используется объект cipher класса PKCS1OAEP_Cipher, который применяет закрытый ключ в алгоритме RSA-­OAEP.

Чтобы протестировать наше приложение, запустите сначала скрипт RsaUdpSender.py, а потом RsaUdpReceiver.py. Получатель должен вывести на консоль переданное сообщение.

По сравнению с симметричными шифрами RSA имеет один существенный недостаток – он работает значительно медленнее. Причина в том, что симметричные шифры используют в своих алгоритмах операции битового сдвига и логическое "или". Современные процессоры обрабатывают их очень быстро за счёт специальных логических блоков, которые способны выполнять по одной операции за такт. Обычная тактовая частота сегодня составляет порядка 2.5 гигагерц (Гц). Это значит, что в секунду процессор способен совершать 2500000000 операций. Наличие нескольких ядер увеличивает это число.

Алгоритмы RSA используют математические функции: возведение в степень по модулю во время шифрования и вычисление функции Эйлера для дешифровки. Их расчёт не может быть ускорен с помощью специальных логических блоков, а потому требует большого числа тактов.

Проблема интенсивных вычислений в асимметричных шифрах решается с помощью сеансового ключа. Идея заключается в том, чтобы сгенерировать временный ключ для симметричного шифрования. В этом случае алгоритм RSA используется только для его безопасной передачи. После этого обе стороны переходят на симметричный шифр и с его помощью защищают свои сообщения. Временный ключ действует до окончания соединения. Для нового соединения он будет сгенерирован повторно.

Вы можете легко изменить скрипты RsaUdpSender.py и RsaUdpReceiver.py так, чтобы вместо строки "Hello world!" передавался сеансовый ключ (например, шифра AES). После этого скрипты смогут перейти на симметричное шифрование для дальнейшего обмена сообщениями.

Асимметричное шифрование позволяет устранить уязвимость, связанную с постоянным хранением секретного ключа на стороне игрового клиента.

Обнаружение внеигровых ботов

Мы рассмотрели криптографические алгоритмы для защиты трафика игрового приложения. Разработчик бота должен потратить достаточно времени на перехват сетевых пакетов и их дешифровку. Предположим, ему это удалось и он написал внеигрового бота для нашей игры. Что мы можем предпринять в этом случае?

На самом деле обнаружить внеигрового бота намного проще чем внутриигрового или кликера. Всё что нужно сделать – это реагировать на получение некорректных пакетов на стороне сервера.

Для примера рассмотрим простейший случай. Мы используем симметричное шифрование и постоянно храним секретный ключ на стороне игрового клиента. Бот импортирует этот ключ и использует его для взаимодействия с сервером. В этом случае обнаружить бота очень трудно. Но у любой онлайн-игры должен быть предусмотрен механизм обновления игрового клиента. Он необходим для исправления ошибок и добавления новых возможностей. Одно из обновлений может менять секретный ключ без уведомления об этом пользователя. Очевидно, что на стороне сервера ключ также будет обновлён. Если после этого бот отправит пакет, зашифрованный старым ключом, сервер не сможет его корректно дешифровать. Таким образом бот себя обнаружит.

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

В случае асимметричного шифрования, мы можем применить тот же подход для обнаружения бота. Есть несколько вариантов распределения ключей. Предположим, что сервер постоянно хранит у себя открытый ключ игрового клиента. В начале сеанса клиент посылает свой открытый ключ. Сервер сравнивает его со своей копией. Если обнаруживается различие, велика вероятность, что пользователь запустил бота. Если ключи совпали, сервер отправляет свой открытый ключ клиенту. После этого они могут шифровать сообщения друг для друга. При обновлении мы генерируем заново все ключи: пару открытый-закрытый на стороне клиента, только открытый ключ клиента на сервере. Если бот попробует воспользоваться старыми ключами, мы его обнаружим.

Если вы не хотите генерировать новые ключи шифрования, есть альтернативное решение. Вы можете регулярно менять протокол игрового приложения. Изменение может быть незначительным. Например, будет достаточно поменять порядок параметров игровых объектов в сетевом пакете или увеличить номер версии протокола. После этого проверив принятый от клиента пакет на соответствие новому формату, будет просто обнаружить бота.

Last updated