[Разработка мобильных приложений, Разработка игр, Unity, Дизайн игр] Еще пять инструментов против читеров на мобильном проекте с DAU 1 млн пользователей

Автор Сообщение
news_bot ®

Стаж: 6 лет 2 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
27-Апр-2021 23:30


Когда-то нам пришлось полностью переработать защиту популярного PvP-шутера. Результатом стал ряд инструментов, которые мы готовили и зарелизили одновременно, чтобы не дать читерам возможность постепенно отслеживать апдейты. Про «первые» пять решений — обфускацию, хранение данных, миграцию прогресса и систему бана — шла речь в этой статье. Сегодня расскажу про остальные, а именно: 
  • Защита от измененных версий.
  • Photon Plugin.
  • Серверная валидация инаппов.
  • Защита от взлома оперативной памяти.
  • Собственная аналитика.
И немного про то, почему так важен был одновременный релиз всех решений.Чтобы не возникло путаницы, продолжим нумерации из прошлой статьи, где уже были опубликованы пять пунктов.Решение №6. Защита от измененных версийВ дополнительные места мы расставили защиту от переподписывания версий, лаунчеров (на Android) и твиков (на iOS), спрятав уже в обфусцированном коде.Проверка на твики (iOS)На устройствах с Jailbreak с помощью Cydia пользователи могут устанавливать твики, которые способны внедрять свой код в системные и установленные приложения. Каждый твик имеет информацию (файл *.plist), с какими бандлами они должны работать.Механизм детекта осуществляется проверкой этих файлов в папке /Library/MobileSubstrate/DynamicLibraries/ (на наличие внутри нашего бандла).Большинство твиков, скрывающих Jailbreak, способно спрятать эту папку от просмотра, поэтому есть способ просматривать ее через созданный нами симлинк.
string finalPath = string.Empty;
string substratePath = "/Library/MobileSubstrate/DynamicLibraries/";
bool bySymlink = false;
if (!Directory.Exists(substratePath)) //Если папки не существует (скрыт твиком xCon), то пытаемся получить доступ к файлам через созданный нами симлинк
{
  string symlinkPath = CreateSymlimk(substratePath);
  if (!string.IsNullOrEmpty(symlinkPath))
  {
    bySymlink = true;
    finalPath = symlinkPath;
  }
}
else
{
  finalPath = substratePath;
}
bool detected = false;
string detectedFile = string.Empty;
try
{
  if (!string.IsNullOrEmpty(finalPath))
  {
    string[] plistFiles = Directory.GetFiles(finalPath, "*.plist"));
    foreach (var plistFile in plistFiles)
    {
      if (File.Exists(plistFile))
      {
        StreamReader file = File.OpenText(plistFile);
        string con = file.ReadToEnd();
        string bundle = "app_bundle";
        if (con.Contains(bundle))
        {
          detectedFile = plistFile;
          detected = true;
          break;
        }
      }
    }
  }
}
catch (Exception ex)
{
  Debug.LogError(ex.ToString());
}
Но также есть твики, которые запрещают создание симлинков по проверяемому нами пути (KernBypass, A-Bypass). При их наличии мы не можем осуществить проверку, поэтому считаем это за возможное читерство.Общего механизма детекта таких твиков нет, тут нужен индивидуальный подход.Детект KernBypass (который был активен в отношении нашего бандла):
if (File.Exists("/var/mobile/Library/Preferences/jp.akusio.kernbypass.plist")
{
  StreamReader file = File.OpenText("/var/mobile/Library/Preferences/jp.akusio.kernbypass.plist");
  string con = file.ReadToEnd();
  if (con.Contains("app_bundle")
  {
    //detected
  }
}
Определение запуска через лаунчер (Android)Запуск приложения через лаунчер — это, по сути, запуск вашего приложения внутри другого приложения (по типу Parallel Space). Некоторые реализации взломов используют такой механизм для внедрения своего кода, и для этого на устройстве не требуется root-доступ. Обычно они имитируют всю среду: выделяют папку под файлы приложения, возвращают фейковый Application Info и так далее.При таком запуске у нас все равно сохраняется доступ ко всем файлам, к которым имеет доступ сам лаунчер. Самый простой способ детекта — это проверить доступ к материнской папке от нашего приложения (dataDir в applicationInfo) через функцию access (в нативном коде). В обычном случае операционная система не предоставит доступ, а в случае лаунчера это будет папка, которая все еще находится внутри Persistent Data приложения.Код для плагина на C:
JavaVM*    java_vm;
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    java_vm = vm;
    return JNI_VERSION_1_6;
}
int CheckParentDirectoryAccess()
{
    JNIEnv* jni_env = 0;
    (*java_vm)->AttachCurrentThread(java_vm, &jni_env, NULL);
    jclass uClass = (*jni_env)->FindClass(jni_env, "com/unity3d/player/UnityPlayer");
    jfieldID activityID = (*jni_env)->GetStaticFieldID(jni_env, uClass, "currentActivity", "Landroid/app/Activity;");
    jobject obj_activity = (*jni_env)->GetStaticObjectField(jni_env, uClass, activityID);
    jclass classActivity = (*jni_env)->FindClass(jni_env, "android/app/Activity");
    jmethodID mID_func = (*jni_env)->GetMethodID(jni_env, classActivity,
                                                      "getPackageManager", "()Landroid/content/pm/PackageManager;");
    jobject pm = (*jni_env)->CallObjectMethod(jni_env, obj_activity, mID_func);
    jmethodID pmmID = (*jni_env)->GetMethodID(jni_env, classActivity,
                                                      "getPackageName", "()Ljava/lang/String;");
    jstring pName = (*jni_env)->CallObjectMethod(jni_env, obj_activity, pmmID);
    jclass pm_class = (*jni_env)->GetObjectClass(jni_env, pm);
    jmethodID mID_ai = (*jni_env)->GetMethodID(jni_env, pm_class, "getApplicationInfo","(Ljava/lang/String;I)Landroid/content/pm/ApplicationInfo;");
    jobject ai = (*jni_env)->CallObjectMethod(jni_env, pm, mID_ai, pName, 128);
    jclass ai_class = (*jni_env)->GetObjectClass(jni_env, ai);
    jfieldID nfieldID = (*jni_env)->GetFieldID(jni_env, ai_class,"dataDir","Ljava/lang/String;");
    jstring nDir = (*jni_env)->GetObjectField(jni_env, ai, nfieldID);
    const char *nDirStr = (*jni_env)->GetStringUTFChars(jni_env, nDir, 0);
    char parentDir[200];
    snprintf(parentDir, sizeof(parentDir), "%s/..", nDirStr);
    if (access(parentDir, W_OK) != 0)
    {
         return 1;
    }
  else
  {
    return 0;
  }
}
Защита от переподписи apk (Android)При любом вмешательстве внутрь apk-файла пакет необходимо переподписать, иначе система не позволит его установить. Поэтому можно определить модифицированную игру, если хеш подписи не совпадает с нашим. Однако такую системную защиту можно отключить, если на устройстве есть root-доступ, и устанавливать пакеты с невалидной подписью. Поэтому данный пункт не является каким-то особенно важным, но и не будет лишним в общей массе.Получение хеша подписи в С# через обращение в Java-код:
Lazy<byte[]> defaultResult = new Lazy<byte[]>(() => new byte[20]);
            if (Application.platform != RuntimePlatform.Android)
                return defaultResult.Value;
#if UNITY_ANDROID
var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
  if (unityPlayer == null)
    throw new InvalidOperationException("unityPlayer == null");
  var _currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
  if (_currentActivity == null)
    throw new InvalidOperationException("_currentActivity == null");
            var packageManager = _currentActivity.Call<AndroidJavaObject>("getPackageManager");
            if (packageManager == null)
                throw new InvalidOperationException("getPackageManager() == null");
            // http://developer.android.com/reference/android/content/pm/PackageManager.html#GET_SIGNATURES
            const int getSignaturesFlag = 64;
            var packageInfo = packageManager.Call<AndroidJavaObject>("getPackageInfo", PackageName, getSignaturesFlag);
            if (packageInfo == null)
                throw new InvalidOperationException("getPackageInfo() == null");
            var signatures = packageInfo.Get<AndroidJavaObject[]>("signatures");
            if (signatures == null)
                throw new InvalidOperationException("signatures() == null");
            using (var sha1 = new SHA1Managed())
            {
                var hashes = signatures.Select(s => s.Call<byte[]>("toByteArray"))
                    .Where(s => s != null)
                    .Select<byte[], byte[]>(sha1.ComputeHash);
                var result = hashes.FirstOrDefault() ?? defaultResult.Value;
                return result;
            }
#else
            return defaultResult.Value;
#endif
Решение №7. Photon PluginДля организации сетевого взаимодействия между пользователями в игровых комнатах мы используем Photon Cloud, который сам по себе предполагает отсутствие серверной логики и отвечает только за пересылку пакетов между пользователями. А для защиты кора нам нужна была именно серверная логика.Переделывать всё на Photon Server было бы достаточно долгой и сложной задачей, так как в игре уже было много различных режимов, механик и прочего. Поэтому, пообщавшись с ребятами из Photon, мы решили попробовать Photon Plugin.Photon Plugin доступен на тарифе Enterprise Cloud и пишется на С#. Он запускается на серверах Photon и позволяет мониторить пересылаемый между пользователями игровой трафик, добавлять серверную логику, которая может: 
  • блокировать или добавлять сетевые сообщения; 
  • контролировать изменения свойств комнат и игроков; 
  • кикать из комнаты;
  • взаимодействовать при помощи http-запросов со сторонними серверами.
Мы переписали сетевое взаимодействие игроков так, чтобы можно было отследить тех, у кого действия выходят за рамки допустимого (например, количество здоровья, урон, скорость перемещения, использование запрещенных предметов и так далее), или код изменен так, что нарушается логика режимов.Про это решение хотелось рассказать подробнее, но тогда бы статья разрослась слишком сильно. Поэтому эту тему оставили для отдельного материала.Решение №8. Серверная валидация иннаповВ нашей игре сотни видов оружия и предметов. Несмотря на баланс и большое влияние личного скилла игроков на результат матча, все инаппы нужно защитить от взлома. Для этого мы ввели валидацию покупок.Валидация на сервере состоит из двух этапов:
  • Превалидация. Когда данные по платежу отправляются на сервер соответствующей платформы для проверки валидности.
  • Начисление. В случае успешно пройденной валидации купленных позиций.
Сначала сервер получает в качестве входных параметров данные, необходимые для проведения валидации (например, на Android — это id инаппа и токен). Отдельной командой/набором команд происходит начисление купленных позиций. Одна позиция может содержать разнотипные итемы (например, деньги и оружие) и для каждого типа итема на сервере существует отдельная команда начисления.Чтобы логически объединить несколько команд, привязанных к одному действию игрока, на клиенте и на сервере мы ввели понятие снапшота. Это специальная конструкция из последовательности команд, которые одновременно выполняются или не выполняются на сервере.Команда валидации проверяет транзакцию — в случае, если есть данные превалидации, то используются они. В противном случае данные отправляются на сервер валидации для соответствующей платформы.В случае успешного начисления id транзакции сохраняется в соответствующий слот игрока — запись в БД, которая хранит данные по платежным транзакциям данного игрока. Во избежание взлома платежки методом, когда одну валидную транзакцию используют для многократного начисления, в рамках валидации осуществляется проверка на существование данного id транзакции.Кроме того, в завершение начисления отправляется соответствующая статистика на сервер аналитики.Подробнее про интеграцию инаппов и серверную валидацию вместе с кодом расскажем в отдельной статье. Решение №9. Защита от взлома оперативной памяти Локальные данные, которые не защищены валидацией с сервера, можно взломать в памяти (например, с помощью GameGuardian на Android). Механизм взлома заключается в поиске значений в памяти путем отсеивания. Ищется текущее значение, затем оно изменяется в игре, а среди найденных адресов в памяти они отсеиваются по новому значению, пока не будет найден нужный адрес.Для их защиты они «засаливаются» при помощи случайно сгенерированной соли:
internal int Value
{
  get { return _salt ^ _saltedValue; }
  set { _saltedValue = _salt ^ value; }
}
Когда пользователь не в состоянии изменить искомое значение для отсеивания, он может попытаться заменить все найденные значения на свои. Для их детекта используется простая ловушка. Следующий пример показывает, как можно определить вмешательство в память с числами от 0 до 1000 (заранее храним массив чисел, которые никогда не должны измениться, кроме как после редактирования памяти).
private static int[] refNumbers;
internal static void Start()
{
  refNumbers = new int[1000];
  for (int i = 0; i < refNumbers.Length; i++)
  {
    refNumbers[i] = i;
  }
}
internal static bool Check()
{
  for (int i = 0; i < 1000; i++)
  {
    if (!refNumbers [i].Equals(i))
      return true;
  }
}
Решение №10. Собственная аналитикаИзначально мы пользовались платным решением от devtodev и бесплатным от Flurry. Основная проблема была в отсутствии детализации происходящих в игре событий. Мы собирали только агрегированные данные и поверхностные метрики.Но с ростом экспертизы внутри команды стала очевидной необходимость писать собственное решение с нужными именно нам фичами. Основная цель была в повышении продуктовых метрик и повороте компании в сторону Data-Driven подхода. Но в итоге аналитика также стала незаменимым инструментом в борьбе с читерами.Например, раньше система не привязывалась к пользователю, а просто считала ивенты. То есть мы знали, что 500 человек совершили какие-то действия, но кто эти 500 человек, и что они делали до этого — нет. Сейчас можно посмотреть все действия каждого конкретного игрока и, соответственно, отследить подозрительные операции.Все пользовательские ивенты отправляются в одну большую SQL-евскую базу. Там есть как элементарные ивенты (игрок залогинился, сколько раз в день он залогинился и так далее), так и другие. Например, прилетает ивент, что игрок покупает оружие за столько-то монет, а вместо суммы написано 0. Очевидно, что он сделал что-то неправомерное.Большинство выгрузки с подозрительными действиями нарабатываются с опытом. Например, у нас есть скрипт, который показывает, что столько-то людей с конкретными id получили определенное большое количество монет. Но это не всегда читеры — обязательно нужно проверять.Также читеров опознаем по несоответствию значений начисления валют. Аналитик знает, что за покупку инаппа начисляется конкретное количество гемов. У читеров часто это количество бывает 9999 — значит, что-то взломали в памяти. Еще бывают игроки с аномальными киллрейтами. По ним у нас тоже есть специально обученное поле, и когда появляется пользователь, у которого киллрейт 15 или 30, становится понятно, что, скорее всего, это читер.В основном отслеживанием занимается один скрипт, который пачкой прогоняет по детектам и сгружает все в таблицу. Аналитики получают id и видят игроков, которые залогинились утром с огромным количеством голды, в соседнем листе лежат игроки, открывшие 1000 сундуков, в следующем — игроки с тысячей гач и так далее. Затем вариантов несколько.Предполагаемых читеров можно прогнать по флагам, а так как подобные способы читерства задетекчены и пофиксены, то подобные игроки уже могли ранее получить значок читера и попасть в отдельную базу. Если они там есть, то бан прилетает автоматически с помощью скрипта.Если нет детекта, данные передаются разработчикам. Они думают, как читер смог обойти защиту и решают, как закрыть эти дыры, как таких игроков детектить и банить.А про причины перехода на собственную аналитическую систему и внутренние фичи тоже расскажем в отдельном материале.Одновременный релиз всех решенийЧтобы достичь максимального эффекта от всех новых защит, было решено выкатить их одномоментно. Так значительно повышался «входной порог» для тех взломщиков, которые уже были хорошо знакомы с нашим необфусцированным кодом.Кроме того, мы не просто применили обфускацию приложения, но и изменили структуру большинства значимых частей приложения. В корне была изменена работа внутри, и все это свело к минимуму имеющиеся к тому моменту у читеров наработки. А разбираться в новом стало куда гораздо сложнее.Всего на глобальный ввод большинства защит ушло около семи месяцев. Самым масштабным пунктом была реализация системы хранения на наших серверах — именно она определяла запуск в продакшен всех решений из списка. Кроме аналитики, которая развивалась самостоятельно.Также было важно развивать приложение в целом и релизить апдейты для игроков, чтобы не потерять их интерес, пока глобальные решения против читеров складывались отдельно в ожидании своего момента.Сейчас игра надежно защищена и не имеет распространенных методов взлома. Иногда встречаются единичные случаи, но они быстро отслеживаются благодаря введенным инструментам.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_razrabotka_igr (Разработка игр), #_unity, #_dizajn_igr (Дизайн игр), #_unity, #_chitery (читеры), #_shuter (шутер), #_pvp, #_gejmdev (геймдев), #_razrabotka_igr (разработка игр), #_gamedev, #_cheats, #_photon_plugin, #_validatsija (валидация), #_blog_kompanii_lightmap (
Блог компании Lightmap
)
, #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
)
, #_razrabotka_igr (
Разработка игр
)
, #_unity, #_dizajn_igr (
Дизайн игр
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 07-Май 09:26
Часовой пояс: UTC + 5