Разработчики игр с удовольствием пользуются теми удобствами, что им предоставляет движок Unity, но, как выяснилось, он столь же удобен и для любого желающего вскрыть эти игры. Для примера я возьму Poker World и покажу на ней, как можно не только менять параметры, но и влезть в саму логику, переиначить все на свой лад и перепаковать APK. Заодно соберем небольшой набор утилит, который поможет в работе с мобильным софтом.
Все манипуляции с приложением делались исключительно в исследовательских целях и не преследуют финансовой выгоды в каком-либо виде.
Работа работой, а отдыхать тоже нужно. Человек я не азартный, но в карты покидать люблю. Недавно я приметил офлайновое приложение для игры в покер под названием Poker World. У него аскетичный интерфейс, минимальный набор функций, но есть почти все, что мне нужно. Напрягал только повышенный интерес разработчиков к донату. Поэтому с каждой раздачей исследовательская жилка напрягалась и таки не выдержала. Я взялся за клавиатуру с твердой уверенностью раздобыть себе немного виртуальных деньжат.
Первые шаги
Чтобы начать вивисекцию приложения нам понадобится небольшой стенд. В первую очередь стоит поставить нормальный эмулятор Android. Раньше я использовал Andy, но теперь он скурвился и даже начал пихать майнеры в свои дистрибутивы. Мне в срочном порядке пришлось искать замену. Моя основная ОС — Windows и поэтому все поиски были направлены на дистрибутивы для этой платформы. Я перебрал с десяток вариантов из топа выдачи Google, и все они, культурно выражаясь, оставляли желать лучшего. Однако мои молитвы были услышаны, и я наткнулся на Memu. На момент написания статьи последняя версия была 5.5.1. Тут есть все, что тебе может понадобиться. Создавать можно виртуальные машины Android версий 4.4 и 5.1, а также поддерживает эмуляцию нескольких моделей устройств.
Также тут присутствуют разные приятные фишки типа эмуляции камеры, спуфинга GPS, мак-адреса и сотового оператора, и, конечно же, есть поддержка трехмерной графики, то есть возможность запускать игры.
Что очень важно, MEmu поддерживает установку приложений из файлов APK и, о боги, в нем есть обычные настройки сетевого подключения Android. Это позволяет легко настроить устройство на работу через прокси типа Burp и перехватывать трафик. Ты удивишься, но часть эмуляторов не поддерживала работу через прокси и не имела стандартных сетевых настроек.
В общем, пока у меня не возникало претензий к MEmu. Если ты знаешь о более достойных вариантах, не стесняйся и пиши о них в комментариях.
Еще нам понадобится APK Studio, чтобы иметь возможность удобно распаковывать, пересобирать и подписывать пакеты. Для подписи пакетов к нему потребуется скачать uber-apk-signer и положить в директорию C:\Users\<имя_юзера>\.apkstudio\vendor, предварительно переименовав в uber-apk-signer.jar. Также можешь обновить apktool до последней версии и скопировать в ту же папку.
И, конечно же, само приложение. Его можно скачать из Play Market и App Store, но нам ведь нужен сам дистрибутив. Тут на помощь приходят сайты-агрегаторы приложений. Такого рода сервисы легко гуглятся по запросу apk download. Я, например, остановился на https://apkpure.com/, где помимо текущей версии приложения можно скачать и предыдущие (иногда бывает полезно).
Переходим по ссылке и скачиваем нашего подопытного. На момент написания статьи последняя версия приложения — 1.3.5.
Теперь можно установить скачанный apk в эмуляторе. Для этого достаточно перетащить его на окно с запущенным MEmu — практически в лучших традициях macOS.
На этом этапе базовые приготовления закончены. Обо всех остальных полезных штуках и утилитах я расскажу в процессе препарирования приложения.
Начинаем веселье в iOS
Так получилось, что играть я начал на айфоне. И где-то на третьем уровне денег начало катастрофически не хватать. Аппарат был без джейлбрейка, и делать мне его совершенно не хотелось, поэтому я начал искать баги в логике самого приложения. Такие, чтобы можно было провернуть, не закапываясь в его дебри.
Парочку таких я и обнаружил. В игре есть возможность раз в четыре часа крутить однорукого бандита, из которого тебе рандомно выпадают всякие ништяки. Их в игре всего два вида — это деньги и билеты. И те, и другие нужны для участия в турнирах. Так вот в местном аналоге тотализатора можно выиграть небольшое количество денег или один билетик. Количество денег, которые ты можешь срубить, зависит от уровня твоего персонажа. Каждые несколько уровней суммы выигрыша в этой рулетке удваиваются. На самом первом они составляют от $400 до $2000.
Про билет и 2000 баксов можно забыть, за месяц игры и тысячи кручений этой рулетки ни разу ничего подобного мне не выпало. Интересно будет взглянуть на алгоритм.
Тем не менее, через пять таких спинов ты получаешь возможность сделать джекпот спин, в котором все выигрыши умножены на три и, соответственно, можно урвать аж три билета.
Баг заключался в том, что время до следующего спина считалось от установленной в системе даты. Поэтому достаточно просто свернуть приложение, перевести часы на четыре часа вперед, развернуть его и снова крутить барабан. Так можно делать до бесконечности.
Чтобы бесконечно не переводить время в будущее, можно сделать так:
- сворачиваем приложение, находясь на экране с таблицей спинов и таймером до следующего;
- переводим часы на четыре часа вперед;
- разворачиваем, видим, что появилась кнопка Spin Now. Нажимать ее не нужно;
- сворачиваем;
- переводим время на четыре часа назад;
- снова разворачиваем и теперь уже нажимаем кнопку.
Я проводил такую процедуру после каждого джекпот спина. Это дает возможность не уходить далеко в будущее, так как от системного времени много чего зависит. Срабатывание алертов, событий в календаре и прочее.
Вообще, я сильно удивился тому, какие проблемы может вызвать изменение даты на устройстве с iOS. Например, после того как я выставил 2030, меня разлогинил iCloud и отказывался пускать обратно с неизвестной ошибкой. Я уж было подумал, что мой аккаунт заблокировали за путешествия во времени. Повторно авторизоваться удалось только после перезагрузки девайса.
Два года назад в iOS нашли баг: устройство окирпичивалось, если поставить дату на 1970 год. Можно предположить, что от системной даты многое зависит.
Плюс сама смена даты неудобна — чтобы сменить год, нужно прокрутить все 365 дней. Вот и сиди, вози пальцем как сумасшедший.
Помимо бесплатных спинов, имеются еще игры без вложений, которые тоже доступны каждые четыре часа. В цепочке — пять игр, в каждой ты можешь выиграть определенную сумму, причем чем дальше по цепочке ты проходишь, тем больше куш. Приз за выигрыш в последней — один билет.
Ускорить можно точно таким же способом — изменение системного времени. К слову сказать, этот стандартный финт с переводом часов еще много где прокатывает, не стесняйся и чекай в тех приложениях, где предлагают чего-то подождать.
Но это все цветочки. У меня завалялся старый джейлбрейкнутый айфон, а это значит, что настало время заглянуть поглубже в недра игры и посмотреть, что там творится. Запускаю утилиту Filza и смотрю, где располагаются файлы приложения. У меня это /path. Немного покопавшись, я обнаружил интересный файл com.youdagames.pokerworld.plist.
Сам файловый менеджер Filza позволяет открывать, редактировать и сохранять файлы plist. В нем располагаются разные настройки, которые касаются игры в целом и текущего пользователя, в частности. Мой взгляд зацепился за парочку интересных переменных с названиями — UserChips, UserTickets. И да, они означают именно то, что ты подумал. Это количество твоих денег (фишек) и билетов. Я поменял значения на нужные и запустил игру.
И вуаля! Виртуальный миллионер. Помимо этого в файле находится куча интересных параметров — таких как текущий уровень игрока, его позиция в десятке лидеров, какие уровни уже пройдены, лимиты столов и прочее. Если будет интересно, то можешь поэкспериментировать на досуге, а я перехожу к версии приложения для Android и более интересным вещам.
Властвуем в Poker World для Android
Первым делом точно так же проверяем домашнюю папку приложения. Не лежат ли там какие-нибудь интересные файлы? Для этих целей я воспользуюсь файловым менеджером из Cyanogen Mod и перейду в папку /data/data/com.youdagames.pokerworld/shared_prefs/.
Сразу же бросается в глаза файл com.youdagames.pokerworld.v2.playerprefs.xml. Откроем его и убедимся: там все то же самое, что и в iOS, только в формате XML.
Но мы это уже проходили, поэтому ценность этой находки для нас минимальна. Пойдем чуть дальше.
Попробуем декомпилировать apk-файл с игрой. Для этих целей я почти всегда использую замечательный онлайновый сервис javadecompilers.com/apk. Кстати, помимо приложений для Android, он может декомпилировать файлы JAR, причем разными методами. На выбор декомпиляторы JDCore, CFR, Jadx и т.д.
В общем, декомпильнули наш файл, скачали его, распаковали и видим, что ничего особо интересного там нет. Печаль.
Также есть целый фреймворк, который неплохо помогает в исследовании мобильных приложений, он называется Mobile Security Framework, MobSF. Он тоже выполняет декомпиляцию приложения и ищет в получившихся исходниках всякие интересные вещи и потенциальные уязвимости. Рекомендую не брезговать им и натравливать его на исследуемые приложения. Фреймворк легко устанавливается и, помимо этого, существует в виде контейнера Docker, что сводит его установку к одной команде.
1 |
[crayon-662a293ab0ac1458078024 inline="true" ]<span class="pun">></span><span class="pln"> docker run </span><span class="pun">-</span><span class="pln">it </span><span class="pun">-</span><span class="pln">p </span><span class="lit">8000</span><span class="pun">:</span><span class="lit">8000</span><span class="pln"> opensecurity</span><span class="pun">/</span><span class="pln">mobile</span><span class="pun">-</span><span class="pln">security</span><span class="pun">-</span><span class="pln">framework</span><span class="pun">-</span><span class="pln">mobsf</span><span class="pun">:</span><span class="pln">latest</span> |
[/crayon]
Но нам это тоже не поможет. У нас здесь игра на движке Unity, на что недвусмысленно намекает наличие характерных файлов.
Я, конечно же, слышал про этот игровой движок, но никогда не приходилось иметь с ним дело в плане реверса. Немного покурив мануалы, я узнал, что все самое интересное обычно находится в файлах Assembly-CSharp.dll, Assembly-CSharp-firstpass.dll и Assembly-UnityScript.dll из папки Managed. Как видно из названия, они написаны на C#, поэтому нам нужен соответствующий декомпилятор. Есть несколько классных вариантов, среди них ILSpy и DotPeek. Обе утилиты абсолютно бесплатны и имеют свои фишки; я остановился на ILSpy. Откроем в нем Assembly-CSharp.dll и Assembly-CSharp-firstpass.dll.
Разворачиваем дерево файла Assembly-CSharp.dll и наблюдаем огромное количество классов с интересными названиями. Это абсолютна вся логика игры. Есть где развернуться!
Но как вносить изменения, чтобы можно было их сохранить? Первый вариант — это экспортировать исходники, отредактировать и попробовать скомпилировать их обратно в библиотеку. Думаю, что при таком раскладе понадобится устранить кучу зависимостей и возникнет немало подводных камней, которые отнимут значительное количество времени — результат просто того не стоит.
Сначала я было расстроился, но немного погуглив, я нашел отличное решение. При помощи утилиты под названием dnSpy мы можем вносить изменения напрямую в DLL, причем так, как если бы мы просто редактировали ее код на C#. Просто магия вне Хогвардса!
Скачиваем программу и открываем наши библиотеки в ней.
Если встроенный редактор покажется тебе недостаточно удобным, можешь сохранить все исходники как проект и пользоваться привычным. Для этого в меню File имеется команда Export to Project.
Сначала изменим что-нибудь простое, чтобы понять, сработает ли. Например, я нашел класс, который отвечает за отображение меню настроек игры. В нем есть метод Show(), который отрабатывает при нажатии на кнопку настроек.
/Assembly-CSharp/SettingsMenu.cs
1 2 3 4 5 6 7 8 9 10 11 |
088: // Token: 0x06001AE2 RID: 6882 RVA: 0x00079D30 File Offset: 0x00078130 089: public override void Show(params object[] args) 090: { 091: base.Show(args); 092: this.facebookText.text = this.GetFacebookText(); 093: base.StartCoroutine(this.Animate()); 094: this._data = GameManager.Instance.LoadSettings(); ... 110: string str = (!GameManager.Instance.cheatsEnabled) ? string.Empty : " | cheats"; 111: string str2 = (!GameManager.Instance.releaseBuild) ? " | debug" : string.Empty; 112: this.version.text = "v" + Application.version + str2 + str; |
Здесь задается переменная this.version.text, которая содержит номер текущей версии игры. Он выводится в левом нижнем углу на экране настроек.
Добавим к этой строчке что-нибудь свое. Для этого нужно кликнуть правой кнопкой мыши внутри метода и выбрать пункт меню Edit Method (C#) или просто нажать Ctrl-Shift-E.
Контекстное меню выбранного метода
Откроется окно для внесения изменений только в выбранный метод. Ты можешь выбрать Edit Class (C#) и редактировать весь класс целиком, но сейчас нам это не нужно.
Окно редактирования выбранного метода в dnSpy
Я добавил переменную str3, в которой написал небольшое приветствие.
/Assembly-CSharp/SettingsMenu.cs
1 2 3 |
111: string str2 = (!GameManager.Instance.releaseBuild) ? " | debug" : string.Empty; 112: string str3 = " | Hello from dnSpy"; 113: this.version.text = "v" + Application.version + str2 + str + str3; |
После этого нажимаем кнопку
Compile. Тут компилятор может вернуть ошибки.
Ошибки компиляции метода в dnSpy
Не будем сильно вдаваться в суть ошибок; посмотрим лучше на место, где они обнаружились. Видишь то же самое имя файла, который мы редактируем, только с постфиксом
g? Это означает global, то есть ошибки (по мнению компилятора dnSpy) содержит основной файл класса. Но мы ведь изменяем лишь один метод, а значит этими ошибками можно спокойно пренебречь. Поэтому двойным кликом по сообщению об ошибке, переходим в место, где они обитают, и просто комментируем неугодные строки.
Устраняем ошибки компиляции метода в dnSpy
После этих действий компиляция прошла успешно. Теперь можешь посмотреть в начало класса и заметить, что те строки, что мы комментировали, находятся в изначальном состоянии.
Состояние класса после рекомпиляции метода
Метод перекомпилирован, теперь идем в меню File и выбираем Save Module.
Таким образом, у нас появилась обновленная DLL. Осталось собрать все обратно в APK, чтобы запустить на телефоне. Хоть APK — это обычный архив ZIP, но просто запаковать и изменить расширение недостаточно. Нужен верный манифест-файл и правильная подпись.
В этих делах нам поможет утилита apkstudio. Откроем APK с игрой и распакуем в какую-нибудь папку при помощи пункта меню File → Open → APK.
Меню File утилиты APK Studio
В опциях открытия файла в Project Path указываем путь, куда складывать распакованные данные. Также можешь убрать галочки с пунктов Decompile Source и Decode Resources, так как нам не нужно декомпилировать исходники и декодировать ресурсы приложения.
Настройки декодирования APK-файла
Теперь жмем Decode и переходим в папку с распакованным приложением. Копируем отредактированный DLL ( Assembly-CSharp.dll) в /assets/bin/Data/Managed с заменой существующей. Снова идем в APK Studio и жмем F5 или выбираем пункт Build из меню Project. В директории, где лежит содержимое APK появился собранная версия. Но просто так установить ее не получится. Остался еще один шаг — подпись.
Сначала нужно сгенерировать ключ. Это можно сделать с помощью входящей в состав Java утилиты keytool. Для создания выполним следующую команду:
1 |
> keytool.exe -genkey -keystore <имя_контейнера> -storepass <пароль> -alias <имя_ключа> -validity <срок_действия_в_днях> -keyalg RSA -storetype pkcs12 |
Возвращаемся в APK Studio, переходим в меню Project → Sign/Export. Выбираем свежесозданный контейнер и указываем пароль к нему, название ключа и его пароль.
Нажимаем Sign. Программа немного подумает и скажет, что все прошло успешно.
Вот теперь можно устанавливать получившееся приложение. Запускаем его, переходим в настройки и видим, что наша строка красуется рядом с версией приложения.
Ну что, теперь пора браться за дела покруче, чем простое добавление строчек.
Немного побродив по исходникам, я наткнулся на интересные классы
Cheat и
CheatMenu. Это менюшка с читами и, судя по методам, есть очень интересные: выиграть текущую игру, показать карты соперников, выиграть текущую игровую зону.
/Assembly-CSharp/CheatMenu.cs
1 2 3 4 5 6 7 8 |
008: public class CheatMenu : MonoBehaviour 009: { ... 169: public void WinPoker(int place) ... 189: public void ShowCards() ... 199: public void CompleteCluster(int clusterNumber) |
Посмотрим в метод Start, чтобы узнать какой код выполняется при запуске
1 2 3 4 |
109: private void Start() 110: { 111: this.Init(); 112: } |
В методе Init происходит инициализация меню.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
079: private void Init() 080: { 081: if (CheatCamera.instance == null) 082: { 083: return; 084: } 085: if (this.transformsToTouch == null || this.transformsToTouch.Length == 0 || this.transformsToTouch[0] == null) 086: { 087: this.transformsToTouch = CheatCamera.instance.transformsToTouch; 088: } 089: this.pointsToTouch = new Vector3[this.transformsToTouch.Length]; 090: for (int i = 0; i < this.pointsToTouch.Length; i++) 091: { 092: this.pointsToTouch[i] = this.transformsToTouch[i].position; 093: } 094: if (GameManager.Instance.releaseBuild) 095: { 096: base.gameObject.SetActive(false); 097: } 098: else if (GameManager.Instance.cheatsEnabled) 099: { 100: base.gameObject.SetActive(true); 101: } 102: else 103: { 104: base.gameObject.SetActive(false); 105: } 106: } |
Пробежавшись по коду, понимаем, что менюшка не работает на релизных версиях приложения. Досадно. Давай исправим это.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
79: private void Init() 80: { 81: if (CheatCamera.instance == null) 82: { 83: return; 84: } 85: if (this.transformsToTouch == null || this.transformsToTouch.Length == 0 || this.transformsToTouch[0] == null) 86: { 87: this.transformsToTouch = CheatCamera.instance.transformsToTouch; 88: } 89: this.pointsToTouch = new Vector3[this.transformsToTouch.Length]; 90: for (int i = 0; i < this.pointsToTouch.Length; i++) 91: { 92: this.pointsToTouch[i] = this.transformsToTouch[i].position; 93: } 94: base.gameObject.SetActive(true); 95: } |
Однако просто активировать чит-меню недостаточно. Обрати внимание на метод Update.
1 2 3 4 5 6 7 8 |
104: private void Update() 105: { 106: if (CheatCamera.instance == null) 107: { 108: return; 109: } 110: this.CheckTouches(); 111: } |
Каждый раз вызывается CheckTouches.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
114: private void CheckTouches() 115: { 116: if (Input.touchCount >= this.pointsToTouch.Length) 117: { 118: Touch[] touches = Input.touches; 119: this.pointsTouched = new bool[touches.Length]; 120: for (int i = 0; i < touches.Length; i++) 121: { 122: Vector3 vector = CheatCamera.instance.GetComponent<Camera>().ScreenToWorldPoint(touches[i].position); 123: Vector3 a = new Vector3(vector.x, vector.y, 0f); 124: foreach (Vector3 b in this.pointsToTouch) 125: { 126: if (Vector3.Distance(a, b) <= 1f) 127: { 128: this.pointsTouched[i] = true; 129: } 130: } 131: } 132: for (int k = 0; k < this.pointsTouched.Length; k++) 133: { 134: if (!this.pointsTouched[k]) 135: { 136: return; 137: } 138: } 139: this.cheats.gameObject.SetActive(true); 140: return; 141: } 142: } |
Судя по телу этого метода, чтобы менюшка показалась, нужно тапнуть в нужных местах. Давай сделаем, чтобы она открывалась сразу после загрузки приложения.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
79: private void Init() 80: { 81: if (CheatCamera.instance == null) 82: { 83: return; 84: } 85: if (this.transformsToTouch == null || this.transformsToTouch.Length == 0 || this.transformsToTouch[0] == null) 86: { 87: this.transformsToTouch = CheatCamera.instance.transformsToTouch; 88: } 89: this.pointsToTouch = new Vector3[this.transformsToTouch.Length]; 90: for (int i = 0; i < this.pointsToTouch.Length; i++) 91: { 92: this.pointsToTouch[i] = this.transformsToTouch[i].position; 93: } 94: base.gameObject.SetActive(true); 95: this.cheats.gameObject.SetActive(true); 96: } |
Сохраняем, билдим, подписываем, устанавливаем, запускаем и видим чит-меню. Можно убедиться, что все кнопочки рабочие.
Но что-то я не вижу в нем самой интересной функции — показать карты соперников. Нужно исправлять это досадное упущение. Посмотрим на то, как выглядит экран во время игры.
В левом нижнем углу есть кнопочка с символом ?, которая показывает, какие карты были на руках в последней раздаче. Давай изменим ее функцию на показ карт соперников. Для этого найдем код, который отвечает за клик по этой кнопке.
/Assembly-CSharp/LastHandMenuButton.cs
1 2 3 4 5 6 7 8 |
05: public class LastHandMenuButton : MonoBehaviour 06: { 07: // Token: 0x0600192D RID: 6445 RVA: 0x0006CF11 File Offset: 0x0006B311 08: public void OnClick() 09: { 10: MenuManager.Instance.Show<LastHandMenu>(new object[0]); 11: } 12: } |
Пересобираем приложение и теперь при нажатии на нужную кнопку будут показываться карты противников.
РЕКОМЕНДУЕМ:
Как писать читы для игр
Получается, ты можешь почти полностью менять логику работы приложения. Можно добавлять фишки прямо при игре на столе, можно выкидывать ботов, влиять на алгоритм раздачи карт и тому подобное.
Выводы
Благодаря таким манипуляциям можно «подправить» почти с любую игру, написанную на Unity, а это, как ты знаешь, очень и очень популярный движок. У dnSpy есть даже специальные билды библиотеки Mono.dll. Заменив оригинальную версию такой библиотеки, можно отлаживать приложение с помощью встроенного в dnSpy дебаггера. Очень удобная вещь.
Лично я получил немало удовольствия от того, что разобрался в логике работы приложения. Надеюсь, тебе тоже понравилось!
Привет, можешь сделать mod, для андроид онлайн-игры War Robots: https://play.google.com/store/apps/details?id=com.pixonic.wwr, чтобы играть только против ботов (как до 4 уровня, когда только начинаешь игру) ? Для этого требуется отредактировать через dnSpy(к примеру) файл Assembly-CSharp.dll и.т.д. MOD должен получится как на видео: https://www.youtube.com/watch?v=FJNWbtgh8GI
пж зделайте софт для gco бесплатный данат и много разных модов