Pull to refresh

Comments 43

UFO just landed and posted this here
Далеко не всем играм нужен сервер. Но даже если игра однопользовательская (например, тетрис), то игроки могут встретиться, к примеру, в рейтингах Play Games или Game Center. Никто не хочет видеть там жуликов.
UFO just landed and posted this here
UFO just landed and posted this here

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 не существует. Можно ссылку на документацию или пример рабочего кода на шарпе?
Вот ещё простой способ конвертации:
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...

Sign up to leave a comment.

Articles