[Перевод] Дорожная карта тестирования безопасности в играх: Поиск уязвимостей в видеоиграх
Введение
В этом руководстве я расскажу, как я создаю инструменты для поиска уязвимостей в видеоиграх для программ вознаграждений за баги. В частности, я сосредоточусь на моих исследованиях игры 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(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 { };
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 предоставляет структуру для выявления уязвимостей в видеоиграх. Используя описанные выше техники, вы можете улучшить свои навыки в поиске эксплойтов и способствовать созданию более безопасной игровой среды. Если у вас есть вопросы или идеи по тестированию безопасности, не стесняйтесь обратиться ко мне!