Введение
В этом руководстве я расскажу, как я создаю инструменты для поиска уязвимостей в видеоиграх для программ вознаграждений за баги. В частности, я сосредоточусь на моих исследованиях игры Sword of Convallaria.
Весь исходный код доступен на GitHub.
Детали игры
Sword of Convallaria доступна на платформках PC и мобильных устройствах, и на данный момент имеет около 2,000 активных пользователей одновременно на Steam (источник: SteamDB). Игра монетизируется через микротранзакции с системой «плати, чтобы победить» и включает PvP-режим. Учитывая ее размер, разработчики должны беспокоиться о возможных уязвимостях, однако у них нет формальной программы вознаграждений за баги.
Игра построена на Unity и использует Lua для большей части игровой логики и обработки. Сетевой протокол использует HTTPS для процесса аутентификации/входа и UDP-пакеты с сообщениями protobuf для общения внутри игры.
Общий план для реверс-инжиниринга
-
Извлечь исходные файлы, чтобы очертить структуру пакетов и перевести ID в строки на английском.
-
Проанализировать процесс аутентификации и сервер лобби.
-
Исследовать игровой трафик и взаимодействие с сервером.
Получение и дампинг данных игры
Так как 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/
Добавить комментарий