Дорожная карта тестирования безопасности в играх: Поиск уязвимостей в видеоиграх

от автора

Введение

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

Весь исходный код доступен на GitHub.

Детали игры

Sword of Convallaria доступна на платформках PC и мобильных устройствах, и на данный момент имеет около 2,000 активных пользователей одновременно на Steam (источник: SteamDB). Игра монетизируется через микротранзакции с системой «плати, чтобы победить» и включает PvP-режим. Учитывая ее размер, разработчики должны беспокоиться о возможных уязвимостях, однако у них нет формальной программы вознаграждений за баги.

Игра построена на Unity и использует Lua для большей части игровой логики и обработки. Сетевой протокол использует HTTPS для процесса аутентификации/входа и UDP-пакеты с сообщениями protobuf для общения внутри игры.

Общий план для реверс-инжиниринга

  1. Извлечь исходные файлы, чтобы очертить структуру пакетов и перевести ID в строки на английском.

  2. Проанализировать процесс аутентификации и сервер лобби.

  3. Исследовать игровой трафик и взаимодействие с сервером.

Получение и дампинг данных игры

Так как Sword of Convallaria разработана на Unity, я использовал существующие инструменты для извлечения данных игры, в частности, AssetsTools.NET, который оказался полезным для этого процесса.

Хотя извлечение файлов не самое увлекательное занятие и хорошо задокументировано, оно открыло несколько интересных файлов Lua и protobuf. Эти файлы содержат все необходимое для создания сетевых инструментов. Вот как я извлекаю все эти важные файлы:

foreach (var luabase in Directory.GetFiles(temp + "unity3d\\lua")) {     var manager = new AssetsManager();     var bunInst = manager.LoadBundleFile(new MemoryStream(File.ReadAllBytes(luabase)), "fakeassets.assets");     var fileInstance = manager.LoadAssetsFileFromBundle(bunInst, 0, false);     var assetFile = fileInstance.file;     foreach (var asset in assetFile.GetAssetsOfType(AssetClassID.TextAsset))     {         var textBase = manager.GetBaseField(fileInstance, asset);         var m_Name = textBase["m_Name"].AsString;         var m_Script = textBase["m_Script"].AsByteArray;         var fileName = temp + @"luac\" + Path.GetFileNameWithoutExtension(luabase) + @"\" + m_Name.Replace("_", @"\\") + ".luac";         if (m_Name.EndsWith(".proto")) fileName = temp + Path.GetFileNameWithoutExtension(luabase) + @"\" + m_Name;         if (File.Exists(fileName)) continue;         Directory.CreateDirectory(Path.GetDirectoryName(fileName));         File.WriteAllBytes(fileName, m_Script);     } }

Давайте углубимся в это.

Преобразование байткода Lua в читаемые скрипты

Байткод Lua кажется зашифрованным, судя по энтропии данных. Чтобы проанализировать его, я подключил функцию slua.dll, ответственную за загрузку Lua-кода. Это позволило мне исследовать загруженный байткод и, что важно, выполнить дамп стека вызовов, чтобы определить метод шифрования. Я обнаружил, что используется простой шифр XOR, при котором первый байт исключается из ротации и XOR’ится отдельно.

data[0] = (byte)(data[0] ^ 0x35);  var key = new byte[] { 0x17, 0xf1, 0xc3, 0x55, 0x78, 0x64, 0x39, 0x40, 0x42, 0x77, 0x59, 0x12, 0x33, 0xcb, 0x7b, 0xb9, 0x35 };  for (var i = 1; i < data.Length; i++)      data[i] = (byte)(data[i] ^ key[(i - 1) % key.Length]); 

С расшифрованным полезным нагрузом Lua я использовал декомпилятор Lua, а именно UnluacNET. Изначально декомпиляция не удалась из-за некорректного магического числа. Я проверил ожидаемое магическое число в slua.dll.

После исправления проверки магического числа я столкнулся с новыми ошибками. Бинарное сравнение между моей собранной slua.dll и версией игры показало различия в функциях чтения, что позволило мне обнаружить еще один слой шифрования.

for (var i = 2ul; i < (ulong)buffer.Length; i++) {     var key = 0x20210507 * i;     var idx = i % 3;     if (idx == 1)         buffer[i] = (byte)(((byte)((key >> 16) & 0xFF) - i) ^ buffer[i]);     else if (idx == 2)         buffer[i] = (byte)(((key >> 21) | i) ^ buffer[i]);     else         buffer[i] = (byte)(((key >> 28) + (key & 1) + i) ^ buffer[i]); }

После исправления функции чтения я все равно столкнулся с проблемами при работе со строками. Дополнительное бинарное сравнение slua.dll выявило важный смещение.

sizeT.m_big -= 10;

Теперь я смог прочитать сырой текст всех декомпилированных Lua-скриптов, которые содержат как игровую логику, так и таблицы данных.

Понимание сетевого протокола

Большинство игр, уделяющих внимание безопасности, блокируют использование общих инструментов, таких как Fiddler, но всегда стоит попробовать, поскольку многие разработчики недооценят безопасность. В данном случае разработчики внедрили некоторые защиты, но поскольку игра построена на Unity, мне удалось относительно легко обойти эти ограничения и включить системные прокси. Существует множество ресурсов, обучающих модификации il2cpp, и я рекомендую их, особенно ключевая часть — это перехват функции HttpClientHandler.SendAsync.

С активным Fiddler я смог наблюдать процесс аутентификации и то, как передаются токены. В данном примере есть базовые идентификаторы клиента, такие как ClientId и AppId, а фактическое содержимое пользователя передается в параметрах POST-запроса. Для гостевых аккаунтов это случайная строка. Для аккаунтов Steam и Google используется стандартный токен, который вы получаете от этих сервисов OAuth. Основной частью ответа на аутентификацию, которая важна, являются AccessToken и MacKey, так как они используются для идентификации на сервере игры.

Игровой трафик можно легко отслеживать с помощью Wireshark. После анализа данных UDP я узнал их как protobuf, что я подтвердил с помощью универсального декодера protobuf (я рекомендую этот). Заголовок пакета обычно включает длину пакета, операционный код (opcode) и иногда другие детали, такие как счетчик или статус шифрования/сжатия. Заголовок пакета выглядит следующим образом:

var length = BitConverter.GetBytes(packetBytes.Length); Array.Copy(length, 0, packetBytes, 0, 4); var opcode = BitConverter.GetBytes((ushort)Enum.Parse<CtoSPacketMessageIds>(packet.GetType().Name)); Array.Copy(opcode, 0, packetBytes, 4, 2); var count = BitConverter.GetBytes(counter); Array.Copy(count, 0, packetBytes, 6, 4); Array.Copy(payload, 0, packetBytes, 10, payload.Length);

Последним шагом было идентифицировать операционные коды (opcodes), которые закодированы в Lua-скриптах в виде таблиц.

foreach (var mode in modes) {     opcodes[mode] = new Dictionary<int, string> { };     foreach (var c2s in Directory.GetFiles("temp\\lua\\pb\\", "*proto.lua", SearchOption.AllDirectories).Where(e => e.Contains(mode)))     {         var luaLines = File.ReadAllLines(c2s);         foreach (var l in luaLines)         {             if (l.Contains(".id = "))             {                 var hasOpcode = int.TryParse(l.Split(' ').Last(), out int opcode);                 if (!hasOpcode || opcode == 0) continue;                 var name = l.Split(' ')[0].Split('.')[1];                 opcodes[mode][opcode] = name;             }         }     } }

Автоматизация обновлений

Когда игра обновляется, важно сделать процесс обновления максимально простым. Это критически важный шаг, чтобы инструменты не выходили из строя каждую неделю. Я включил в проект функцию скачивания сырых файлов ассетов непосредственно с игровых серверов в Downloader.cs.

Собираем все вместе

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

Вот пример теста, который я провел, чтобы проверить базовую функциональность Gacha.

await client.SendPacket(new CSOnlineGacha { Id = 2, Times = 10, Consume = new DBConsume { Type = 114, Param0 = 1, Param1 = 10 } });

Это место, где я бы проверил отрицательные числа, странные паттерны и другие необычные случаи.

Если бы у меня было бесконечно много времени, я также проверил бы более низкоуровневые уязвимости, такие как ошибки при разборе protobuf.

Заключение

Этот гид по тестированию безопасности игры Sword of Convallaria предоставляет структуру для выявления уязвимостей в видеоиграх. Используя описанные выше техники, вы можете улучшить свои навыки в поиске эксплойтов и способствовать созданию более безопасной игровой среды. Если у вас есть вопросы или идеи по тестированию безопасности, не стесняйтесь обратиться ко мне!


ссылка на оригинал статьи https://habr.com/ru/articles/857994/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *