Как стать автором
Обновить

Комментарии 43

НЛО прилетело и опубликовало эту надпись здесь
Далеко не всем играм нужен сервер. Но даже если игра однопользовательская (например, тетрис), то игроки могут встретиться, к примеру, в рейтингах Play Games или Game Center. Никто не хочет видеть там жуликов.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Il2cpp не спасает от frida, как и обфускация кода.
Пока из хитрожопого встречал защиту pokemon go, которая роняет аппку при попытке заинжектить код.
А может, я просто что-то не понял.

Даже крупные студии не могут сделать 100% защиту от модификации кода. И никакие обфускаторы и эмуляторы не помогают. Все новые игры сразу появляются на торрентах с выпиленной защитой. Тут проще изменить подход к игровой экономике. Если нельзя перенести ее целиком на сервер, то нужно хотя бы минимизировать влияние игроков друг на друга. Лимиты на операции, лимиты на цены и прочее. Это как с кредитными картами — даже если вы украдете чужую кредитку и CVV от нее, то снять/потратить много денег у вас не получится.
А зачем в самом файле писать #define NEWTONSOFT_JSON, если в настройках есть специального для этого Scripting Define Symbols?
Кому как удобнее. В коде оно очевиднее.
И если таких модулей с прописанными в коде дефайнами много, то потом бегать по каждому файлу и вырезать? =/

Если что, есть еще более удобная штука, такая как csc.rsp

BitConverter — пожалуй, самый неторопливый способ интерпретировать массив как long.
Код, опять же, повторяется.


Возможно, стоит переписать так:


[Serializable]
public class ProtectedValue<T> : Protected where T : struct
{
#if NEWTONSOFT_JSON
    [JsonConstructor]
#endif
    private ProtectedValue() { }

    protected ProtectedValue(ReadOnlySpan<byte> bytes) : base(bytes) { }

    public static implicit operator ProtectedValue<T>(T value)
    {
        return new ProtectedValue<T>(MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateReadOnlySpan(ref value, 1)));
    }

    public static implicit operator T(ProtectedValue<T> value) => value == null ? default(T) : MemoryMarshal.Cast<byte, T>(value.DecodedBytes)[0];

    public override string ToString()
    {
        return ((T)this).ToString();
    }
}

public abstract class Protected
{
#if NEWTONSOFT_JSON
    [JsonProperty]
#endif
    [SerializeField]
    private readonly byte[] _;

    private static readonly byte[] Key = System.Text.Encoding.UTF8.GetBytes("8bf5b15ffef1f485f673ceb874fd6ef0");

    protected Protected()
    {
    }

    protected Protected(ReadOnlySpan<byte> bytes)
    {
        _ = Encode(bytes);
    }

    private static byte[] Encode(ReadOnlySpan<byte> bytes)
    {
        var encoded = new byte[bytes.Length];

        for (var i = 0; i < bytes.Length; i++)
        {
            encoded[i] = (byte)(bytes[i] ^ Key[i % Key.Length]);
        }

        return encoded;
    }

    protected byte[] DecodedBytes
    {
        get
        {
            var decoded = new byte[_.Length];

            for (var i = 0; i < decoded.Length; i++)
            {
                decoded[i] = (byte)(_[i] ^ Key[i % Key.Length]);
            }

            return decoded;
        }
    }
}
System.Memory не поддерживается в Unity, так что не получится использовать MemoryMarshal. И ReadOnlySpan тоже недоступен.
Практически то же самое, только копипасты больше за счет отсутствия базового класса. Но все равно здорово, что кто-то тоже озаботился подобной проблемой)

Советую сравнить производительность — она будет далеко не "такой же". :)

Каждый решает свои задачи. В моем случае это не имеет вообще никакого значения. Особенно в сравнении с другими процессами, которые влияют на производительность в миллионы раз сильнее.
Я новичок в геймдеве (хотя и пилю в свободное время свою браузерную MMORPG уже 4 года), но возникает банальный вопрос — если это сингл-игра — то зачем заморачиваться с защитой? Ну захотел человек взломать — пусть взламывает. Вон, в играх серии The Elder Scrolls разработчики даже консоль сделали — делай со своим персонажем и игровым миром что хочешь.

А если игра сетевая — то, логично, что всю механику нужно просчитывать на сервере. Пользователь как угодно может ломать свой клиент — на сервере его параметры здоровья, золота и прочего никак не изменятся.

Есть другая проблема онлайн игр (которая в одних жанрах проявляется слабее, например в шутерах, в других — сильнее, например в mmorpg), которая до конца не решена никем — как защитить игру от автоматизации (ботов и скриптов). Вот на эту тему было бы намного интереснее почитать статьи и варианты решений.
Аналогинчо не понимаю. Мне часто нравится именно взламывать игрушку. Сам процесс интересен. Ну и потом можно придумывать тактики проверять шмот, не мучаясь с долгим фармом. Ну и есть игры, которые слишком сильно заставляют фармить — тоже интереснее играться после взлома, сосредоточившись на других механиках.
Если в игре есть внутриигровые покупки, то пользователь не будет их покупать, а просто скачает взломанную версию игры или использует сторонее ПО для модификации.
А если написать protected, то он пойдет и купит гору алмазов за 7000 рублей, ага.
Если честно, мне кажется множество ломающих игры, и множество покупающих игровую валюту если и пересекаются, в очень небольшой площади.
Защита приложения — комплекс мер, при этом не только технических. В статье рассмотрен только один из аспектов, который, конечно же, не обещает решить все ваши проблемы. Согласен, что жулики в любом случае не будут ничего покупать, если речь только про IAP. Зачастую, приходится изолировать таких пользователей от честных игроков, чтобы они не мешали друг другу.

Игровые покупки, рейтинги и достижения. Иногда ограниченное взаимодействие пользователей, например, рынок (обмениваются игроки на сервере, но добывают вещи в оффлайне).

обмениваются игроки на сервере, но добывают вещи в оффлайне

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

Вот вроде бы задумка хорошая, но превращать value type (int) в reference type (ProtectedInt) — ужасная идея. При передаче числа в функцию вы не ожидаете, что оно может измениться в результате выполнения функции.


Поменяйте class на struct, и храните результат вычислений в ubyte/uint/ulong — и будет вам счастье.


А чтобы быстрее работало, используйте math.asuint / math.asulong для конвертации. Массив байт вам явно не нужен.

Согласен, сделал апдейт. Для Int32 убрал BitConverter, для float пока ничего умного не придумал.

Ничего умного и не надо:) Делаешь то же самое, что и с int32, но при записи делаешь math.asuint, а при чтении делаешь math.asfloat. По сути это reinterpetcast.

Речь всё ещё о c#? Таких функций как math.asuint и math.asfloat в Math не существует. Можно ссылку на документацию или пример рабочего кода на шарпе?
Похоже, что речь о Unity-specific API.

Я уточнил в ЛС, это отдельный пакет для Unity. Я решил обойтись без него, хотя с его помощью можно немного ускорить код после замены вызовов BitConverter.
https://docs.unity3d.com/Manual/com.unity.mathematics.html

Вот ещё простой способ конвертации:
public static unsafe int SingleToInt32Bits(float value)
{
    return *(int*) &value;
}
        
public static unsafe float Int32BitsToSingle(int value)
{
    return *(float*) &value;
}

Думаю под капотом math.asfloat скрывается тоже самое.

Всё верно.

Спасибо! Однако, в Unity unsafe отключен по умолчанию. Не стал бы рекомендовать всем включать его ради такой функции.

Не очень понятно почему. Unity топит сейчас за ECS и все больше фич переезжает под него, а движок ECS как раз на unsafe и построен.

Создаёте Assembly Definition, настраиваете зависимости, включаете unsafe. В итоге получаете более быструю компиляцию. И unsafe не надо включать для всего проекта.

Тоже верно:)

Сколько раз я в детстве ломал игры через ArtMoney поиском неизвестного значения, по принципу изменилось, не изменилось, не изменилось, изменилось, не изменилось… А потом поиск указателя на начало блока, и в 90% даже искать заново потом не надо. И xor тут не помогает...

Надо думать над вариацией с двумя полями)

Причем они должны идти не подряд что бы не выглядеть например как 64 разрядное число вместе. Или периодически перекодироваться вне зависимости от изменения данных.


И все равно защита будет так себе, ибо переводить на такой бред все ресурсы это хмм. А иначе всегда можно будет взломать не деньги, но 100500 алмазов например.

Достаточно будет рандомно менять "ключ" xor'а при записи значения. И при этом рандомно решать, менять ли "ключ" или использовать старый (для отсечения ситуаций когда после фильтраций остаются два неизвестных значения во всём адресном пространстве, и их оба одновременно замораживают).

Рандомно решать не поможет, если взломщик сможет их интерпретировать как одно значение (например int32 key + int32 value = int64 view). Просто процесс поиска станет вероятностным.


Например: поиск, десять разный действий с другими данными и поисков на не изменение. Потом 10 разных покупок на деньги (что бы 50%^10 что ключ не поменялся) и 1 поиск. Повторить раза 3...

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации