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

bab5d866cad23d107a169d3370de1db7.webp

Введение

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

© Habrahabr.ru