Как создавать патчи, основанные на доверчивой политике безопасности Android

    Приветствую тебя, уважаемый читатель!


    Offtop

    В этом топике я хотел бы рассказать о том, как можно получать доступ к тому, что принадлежит нам, но косвенно.
    Любой софт — это чья-то собственность: кто-то сидел и шевелил своими мозгами, чтобы произвести на свет очередное «чудо».
    Но вне наших девайсов речи о софте не может и быть. Понятие «софт», в таком случае, будет означать не более чем «мягкость».
    Это очевидно.
    Именно поэтому мы имеем право вытворять с любым программным кодом, выполняющимся на нашем устройстве, все, что пожелает душа…

    Intro

    Думаю, начать стоит с самого начала. Все началось с моего обнаружения пользовательской зависимости окружающих к небезызвестной игре 2048. Мне, конечно же, стало интересно, чем эта игра так захватывает людей, поэтому я решил сам ее попробовать. Через несколько минут я понял ее суть и мне эта игра тоже стала интересна.
    Должен признаться, более 5000 очков мне набирать не удавалось никак, а до 2048 было еще слишком долго…
    Тем временем окружающие «игроки» с легкостью набивали по 10-16тыс очков, а некоторым удавалось даже достигать 2048 в клетке.
    Что ж, спустя некоторое время у меня возникла идея тупо взломать эту игру, чтобы не «убивать» время на ее прохождение( она слишком долгая в прохождении, но от этого не менее затягивающая ). А еще спустя пару дней, об этом же меня попросили и мои знакомые.
    Это означало одно: пора браться за дело…





    Что было в начале?

    Этот взлом не начался с чего-то необычного. Все было по шаблону: я вытащил APK-пакет приложения, произвел первичную декомпиляцию, с использованием apktool.jar, затем вторичную( для получения Java кода из Smali ), используя тулзу jd-gui. Ничего необычного. Далее я долгое долгое время бороздил по Java-классам в надежде отыскать хоть что-то полезное и интересное. Помимо «тонн» рекламных и гугловских библиотек я ничего не замечал. В конце концов удалось наткнуться на внутренности данного приложения, т.е то, где все интересное и происходит:

    image

    Но, как ты можешь видеть, рыться во всем этом, видимо, обфусцированном коде, мало кому захочется: оригинальные названия классов, методов и переменных представляют из себя лишь краткий набор букв. Исследование на данном этапе зашло в тупик…
    Я решил бросить это дело и заняться чем-то более полезным. Но спустя несколько дней я решил вновь вернуться к данной теме.

    Начало конца

    Я вдруг решил просто покопаться в данных, хранимых приложением. Это могли быть данные обо всем, начиная с цвета клеток поля и заканчивая текущим количеством очков. Структура данных приложения такова:



    Теперь по порядку:

    • Папка /cache пуста, а поэтому особого интереса не представляет
    • В /files, на первый взгляд, хранится мусор, но это только на первый взгляд
    • Папка /lib содержит графическую библиотеку Cocos2D, что, в общем-то, нам тоже не интересно.
    • Ну и /shared_prefs содержит текущие значения SharedPreferences приложения


    Из этого мы делаем вывод, что интерес представляют только две папки: files и shared_prefs.
    Что ж, смотрим, что там внутри них.

    image

    image

    В первом случае нас интересует файл save.plist, а во втором — единственный файл Cocos2dxPrefsFile.xml.
    Их названия говорят за себя. Дабы не растягивать текст, сразу предоставлю информацию об обоих файлах:

    1) save.plist
    Как несложно догадаться, этот файл отвечает за сохранение состояния игры перед выходом. В сохранение состояния входит: описание клеток игрового поля, количество Undo и текущий рейтинг игрока.

    2) Cocos2dxPrefsFile.xml
    Здесь приложение хранит данные о максимально достигнутом когда-либо количестве очков.


    Одна из особенностей заключается в том, что эти файлы представлены в удобочитаемом формате XML:

    1) Cocos2dxPrefsFile.xml

    <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
    <map>
    <int name="Score" value="4846" />        // максимальный рейтинг
    <int name="ScoreSent" value="2410" />
    <int name="BestBoxValue" value="512" />
    <boolean name="FirstTime" value="true" />
    </map>
    


    2) Файл save.plist
    ( каждый тег dict хранит данные об определенной клетке в определенный момент времени )

    
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"/>
    
    <plist version="1.0">
        <dict>
            <key>Main</key>
            <array>
                <dict>                // Отдельно взятая клетка поля
                    <key>Index</key>                     
                    <string>1</string>            // Номер клетки( 0..15 )
                    <key>Level</key>                  
                    <string>3</string>            // Степень двойки в клетке
                    <key>Score</key>
                    <string>1204</string>     //  Текущий рейтинг
                    <key>MaxUndo</key>
                    <string>2</string>            // Undo
                </dict>
                <dict>
                    <key>Index</key>
                    <string>4</string>
                    <key>Level</key>
                    <string>3</string>
                    <key>Score</key>
                    <string>1204</string>
                    <key>MaxUndo</key>
                    <string>2</string>
                </dict>
                <dict>
                    <key>Index</key>
                    <string>0</string>
                    <key>Level</key>
                    <string>1</string>
                    <key>Score</key>
                    <string>1204</string>
                    <key>MaxUndo</key>
                    <string>2</string>
                </dict>
                <dict>
                    <key>Index</key>
                    <string>3</string>
                    <key>Level</key>
                    <string>1</string>
                    <key>Score</key>
                    <string>1204</string>
                    <key>MaxUndo</key>
                    <string>2</string>
                </dict>
                <dict>
                    <key>Index</key>
                    <string>2</string>
                    <key>Level</key>
                    <string>2</string>
                    <key>Score</key>
                    <string>1204</string>
                    <key>MaxUndo</key>
                    <string>2</string>
                </dict>
                <dict>
                    <key>Index</key>
                    <string>14</string>
                    <key>Level</key>
                    <string>2</string>
                    <key>Score</key>
                    <string>1204</string>
                    <key>MaxUndo</key>
                    <string>2</string>
                </dict>
            </array>
            <key>Steps</key>
            <array>
                <dict>
                    <key>Main</key>
                    <array>
                        <dict>
                            <key>Index</key>
                            <string>1</string>
                            <key>Level</key>
                            <string>10</string>
                        </dict>
                        <dict>
                            <key>Index</key>
                            <string>4</string>
                            <key>Level</key>
                            <string>10</string>
                        </dict>
                        <dict>
                            <key>Index</key>
                            <string>0</string>
                            <key>Level</key>
                            <string>1</string>
                        </dict>
                        <dict>
                            <key>Index</key>
                            <string>3</string>
                            <key>Level</key>
                            <string>1</string>
                        </dict>
                        <dict>
                            <key>Index</key>
                            <string>2</string>
                            <key>Level</key>
                            <string>1</string>
                        </dict>
                    </array>
                </dict>
                <dict>
                    <key>Main</key>
                    <array>
                        <dict>
                            <key>Index</key>
                            <string>13</string>
                            <key>Level</key>
                            <string>10</string>
                        </dict>
                        <dict>
                            <key>Index</key>
                            <string>12</string>
                            <key>Level</key>
                            <string>10</string>
                        </dict>
                        <dict>
                            <key>Index</key>
                            <string>8</string>
                            <key>Level</key>
                            <string>1</string>
                        </dict>
                        <dict>
                            <key>Index</key>
                            <string>15</string>
                            <key>Level</key>
                            <string>1</string>
                        </dict>
                        <dict>
                            <key>Index</key>
                            <string>14</string>
                            <key>Level</key>
                            <string>1</string>
                        </dict>
                        <dict>
                            <key>Index</key>
                            <string>10</string>
                            <key>Level</key>
                            <string>1</string>
                        </dict>
                    </array>
                </dict>
            </array>
        </dict>
    </plist>
    
    
    


    Теперь, зная это и имея на устройстве ROOT права, можно с легкостью подстроить игру под себя.
    Но рут доступ есть далеко не у всех.
    Именно поэтому следует создать патч для этого приложения, который был бы доступен широкой аудитории.

    Создание патча

    Для начала немного теории.
    Все ANDROID-приложения обладают собственной песочницей, доступ к которой может быть получен только этим приложением( или рут-пользователем ). Песочница представляет из себя папку, находящуюся в сердце OS — /data/data/*. Вместо звездочки может стоять имя пакета приложения. Например, имя пакета игры 2048 — com.estoty.game2048, как ты мог уже догадаться из слайдов выше. Следовательно, доступ к папке /data/data/com.estoty.game2048 есть только у игры( и у рута ), а, следовательно, и доступ ко всем вкусностям, перечисленным выше.
    Казалось бы, что нам остается?

    Вероятно, тебе приходит на ум создание собственного приложения с одноименным пакетом. Но при компиляции и установке нашего приложения-фейка мы получим ошибку INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, говорящую о том, что ключ, которым была подписана игра, не соответствует ключу, которым подписано наше приложение-фейк.

    Значит, нам остается одно: пересобрать игру в APK и подписать его нашим ключом с помощью тулзы
    JARSIGNER, а затем и наше приложение-фейк тем же ключом! Плюс, зная то, что при переустановке приложений Android не удаляет данные игры( т.е ее песочницу ), мы можем подменить данные игры на свои данные, опираясь на добрую и доверчивую политику безопасности OS Android, а затем запустить оригинальную игру, которая бы уже использовала наши фейковые данные.
    Но а вдруг игра перезапишет данные при переустановке? Что ж, сейчас и это проверим!

    Начнем с подписки игры нашим ключом( сертификатом ). Для этого ее нужно стандартно разобрать( декомпилировать, если угодно ), а затем собрать снова и уже после этого подписать своим ключом. Предполагается, что ты знаком с этим процессом. Если же нет, то он неплохо расписан, например, вот здесь.

    jarsigner -keystore default.keystore -storepass *** -keypass *** 2048.apk default


    Итак, имеем APK пакет приложения, подписанный нашим дефолтным ключом.

    Далее мы создаем новое Android-приложение, с именем, названием пакета и сертификатом(ключом), идентичным игре 2048.
    Теперь между нашим приложением-патчем и игрой 2048 нет каких-либо отличий на системном уровне ( то есть для Android, это два идентичных приложения, которые могут друг друга переустановить, заменять ),

    Теперь нам следует подумать о том, как будет работать патч.
    Разумеется, все можно сделать просто: писать статические данные в файлы Cocos2dxPrefsFile.xml и save.plist, которые, например, устанавливают большое количество очков и большие числа на игровом поле. Но это не круто. Я предлагаю сделать патч динамичным, т.е таким, чтобы он мог в любое время без перекомпиляции( пересборки ) устанавливать нужные нам значения рейтинга, клеток поля и т.д.

    Если поплевать на дизайн и оформление, а сосредоточиться только лишь на функциональности патча, то получится примерно следующее:

    image

    В патч включены самые «полезные» фичи:
    у пользователей будет возможность менять свой лучший рекорд, менять текущий и устанавливать цифры в каждой клетке игрового поля!
    Это более чем достаточно.

    Коддд

    P.S Предполагается, что ты имеешь базовые представления об устройстве Android-приложений. Поэтому комментариев к коду практически нет.

    Итак, с манифестом приложения мы ничего более делать не будем — имя пакета уже было установлено при создании нового проекта, а имя приложения, в общем-то, менять необязательно. Никаким особым функционалом патч обладать не будет, так что специфичные user-permissions нам тоже ни к чему.

    По нажатию на единственную кнопку будет вызываться функция Patch:

        private void Patch()
        {
            SharedPrefsPatch();  // 1
            FilePatch();                // 2
            AlertDialog.Builder alert = new AlertDialog.Builder(this);
            alert.setMessage("OK. Now you should install the original game without removing this app.");
            alert.setTitle("Success!");
            alert.setCancelable(false);
            alert.setPositiveButton("Ready!", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.cancel();
                    Main.this.finish();
                }
            });
            alert.show();
        }
    


    Устанавливаем новый рекорд:

    //1
        private void SharedPrefsPatch()
        {
            SharedPreferences prefs = getSharedPreferences("Cocos2dxPrefsFile", Context.MODE_WORLD_READABLE);
            SharedPreferences.Editor editor =  prefs.edit();
            editor.putInt("Score", Integer.parseInt(bestscore.getText().toString()));
            editor.putInt("ScoreSent", Integer.parseInt(bestscore.getText().toString()));
            editor.putInt("BestBoxValue", 1024);
            editor.putBoolean("FirstTime", true);
            editor.apply();
        }
    


    И формируем игровое поле, как возжелаем( как ты помнишь, тут все в формате XML. Прошу прошения за небрежность формата ниже. ):

    
    // 2
     private void FilePatch()
        {
            try {
    
                FileOutputStream fop = openFileOutput("save.plist", MODE_WORLD_READABLE);
                OutputStreamWriter writer = new OutputStreamWriter(fop);
    
                writer.write("" +
                        "" +
                        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                        "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"/>\n" +
                        "\n" +
                        "<plist version=\"1.0\">\n" +
                        "    <dict>\n" +
                        "        <key>Main</key>\n" +
                        "        <array>" +
                        "" +
                        "");
    
                for(int i=0;i<textboxCells.size();++i)
                    if(textboxCells.get(i).getText().toString().trim().length()>0)
                    writer.write("" +
                            "" +
                            "<dict>\n" +
                            "                <key>Index</key>\n" +
                            "                <string>"+i+"</string>\n" +
                            "                <key>Level</key>\n" +
                            "                <string>"+textboxCells.get(i).getText()+"</string>\n" +
                            "                <key>Score</key>\n" +
                            "                <string>"+currentscore.getText().toString()+"</string>\n" +
                            "                <key>MaxUndo</key>\n" +
                            "                <string>200</string>\n" +
                            "            </dict>" +
                            "");
    
                writer.write("" +
                        "" +
                        "        </array>\n" +
                        "        <key>Steps</key>\n" +
                        "        <array>\n" +
                        "            <dict>\n" +
                        "\n" +
                        "            </dict>\n" +
                        "        </array>\n" +
                        "    </dict>\n" +
                        "</plist>" +
                        "");
    
                writer.flush();
                writer.close();
    
            } catch (FileNotFoundException e) {
                Log.i("ERRORMINOR", "********1");
            } catch (IOException e) {
                Log.i("ERRORMINOR", "********2");
            }
    
    


    Компилируем. Подписываем( сертифицируем ) тем же ключом, что и игру 2048.

    Проверяем работоспособность

    1) Сперва установим игру 2048( с помощью ADB, для начала ):
    adb install 2048.apk


    image

    и запустим:

    image

    Супер, игра, подписанная неродным ключом, работает.

    2) Устанавливаем патч( Android автоматически переустановит приложение, заменит его на патч, данные приложения, при этом, сохранятся ):
    adb install -r patch.apk

    image

    и эксплуатируем его:

    image

    патчим:

    image

    3) Переустанавливаем нашу игру:
    adb install -r 2048.apk


    и запускаем:

    image

    Отлично. Патч полностью работоспособен!
    В данный момент мы все действия проводили в консоли( для скорости ), но на деле же все куда проще: достаточно закинуть и игру, и патч на карту памяти своего девайса, например, далее установить игру, затем патч и затем снова игру. В таком случае система Android тебя каждый раз будет предупреждать, что приложение будет переустановлено. Вы, разумеется, соглашаетесь.

    Вот так можно использовать политику безопасности Android, наложенную на пакеты приложений, для создания патчей.

    Тут можно скачать APK подписанной игры.

    А вот тут патч( тоже подписанный )

    Outro

    На самом деле, подобная брешь не является виною только Android. Я думаю, тут также виноваты и разработчики приложений, которые не проверяют целостность данных, не шифруют их и используют их в своем приложении с полной доверенностью источнику…
    Что ж, вот мы и рассмотрели еще один аспект безопасности, который следует учитывать при разработке своих приложений под OS Android.

    До новых встреч!
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      +3
      Но зачем? Суть любой игры в том, чтобы пройти её, а не выиграть.
        +1
        Действительно, это так. Но здесь акцент, все же, больше делается именно на способ и его реализацию. Вместо этой игры, разумеется, могла быть и любая другая. Я имею ввиду, что взлом, как таковой, здесь не является основной темой.
        +3
        Я думаю, тут также виноваты и разработчики приложений, которые не проверяют целостность данных, не шифруют их и используют их в своем приложении с полной доверенностью источнику…

        На одного игрока, который хакнет игру, найдется 1000-10000 тех, кто просто поиграет в нее и скажет спасибо (или даже $). Какой смысл тратить тонну времени на защиту от взлома таких игр, если за это время лучше сделать уровней или запилить еще один режим игры? Любую защиту все равно сломают, ломать — не строить.
          +1
          Ломать — не стоить, это уж точно…
          +2
          На самом деле, подобная брешь не является виною только Android.

          Эээ, где же брешь-то?
          Приложение, подписанное новым сертификатом — это новое приложение. Поэтому ваш пропатченный 2048 — это уже не оригинальное приложение, его нужно ставить отдельно, и никаких апдейтов из маркета оно уже не получит.
          Точно так же можно было разобраться в коде и сделать так, чтобы пропатченный 2048 сам писал нужные данные в файлы.
          А то, что два приложения подписанные одним ключом (то есть принадлежащие одному разработчику), могут тесно взаимодействовать — это заложено в Андроид изначально (например, см. android:process в манифесте).

          Имхо, брешью будет, если поставить патч, сгенерить файлы, снести патч, а затем поставить оригинал, и он вдруг подхватит файлы от патча.
            0
            Может я чего-то недопонимаю, но разве в статье как раз не об этом написано?
              +1
              Итак, имеем APK пакет приложения, подписанный нашим дефолтным ключом.

              То есть фактически имеем своё приложение и делаем с ним что хотим (выпускаем апдейты, взаимодействуем с другими нашими приложениями). Естественно это приложение не может быть установлено поверх такого же приложения другого издателя.
            +1
            Ребят, ну это вовсе не патч. Мало апк переписать, так еще и XML сохранялки меняют.
              +3
              эм… ну отредактировали сейв. И? При xml-формате это просто. вот у diablo1 сложный формат save'a был, я не. осилил на первом курсе.
                0
                Был редактор сейвов и для 1 и для 2.
                  +2
                  В мою скудную юность интернетов ещё толком не было, так что сейвы ломались вручную, при помощи hexeditor'а, diff'а и прочих handy stuff.
                  0
                  А ведь plist бывает и бинарный
                    0
                    Да-да, полностью с вами согласен. Приложениям, действительно, следует хранить свои более-менее важные данные не в чистом виде, а хотя бы в бинарном, как это делают разработчики Subway Surfers. Вот их уже будет гораздо сложнее «проломить» и сфальсифицировать данные. Собственно, эта статья, отчасти, как раз к подобному подходу хранения данных и призывает.
                      +5
                      В чём смысл прятать сейвы от пользователя?
                    +1
                    Так пропатчить данные приложения, установленного из магазина мы не можем, верно, только «своего» приложения (переподписанного)?
                      0
                      Тут все дело в сертификате( цифровом ключе ) приложения. Собственно, достать сам APK пакет можно откуда угодно — хоть с GooglePlay — структура пакетов везде одинаковая. Но только после «взятия» целевого пакета уже встает вопрос о его переподписке.
                        0
                        Я к тому, что как вариант атаки («заразить чужой телефон, которого нет у вас в руках») не сработает. Уже неплохо.
                      +1
                      И играть стало неинтересно :)
                        +3
                        Я, одно время, использовал в своих играх решение с шифрованием SharedPreferences, типа такого, что впрочем, не особо усложняет. Сейчас не использую никакой защиты. Смысл? Захотят взломать — взломают. Игрок, делающий патч с «бесконечными патронами» (или качающий этот патч с какого-нибудь варезника) — в любом случае, не моя ЦА. Лучше я потрачу это время/усилия на улучшение самой игры.
                          0
                          Неожиданно увидеть plist на Android. С другой стороны, там же cocos2d.
                            +3
                            Изобретение
                            image
                              –1
                              «Что ж, спустя некоторое время у меня возникла идея тупо взломать эту игру, чтобы не «убивать» время на ее прохождение( она слишком долгая в прохождении, но от этого не менее затягивающая ). А еще спустя пару дней, об этом же меня попросили и мои знакомые. »
                              зачем?
                                +2
                                И второй вопрос: игра отправляет данные на сервер для топа игроков? Т.е., реально фейковый рекорд себе сделать?
                                Если нет, то никаких проблем в безопасности нет.
                                  0
                                  а затем запустить оригинальную игру, которая бы уже использовала наши фейковые данные.

                                  Только не оригинальную игру, а переподписанную нашим ключом — правильно?
                                    0
                                    Да, разумеется. «Уникальность и неповторимость» приложений на уровне системы определяется для Android как минимум двумя основными аспектами: название JAVA пакета( Java Package) и сертификатом( ключом ). Получается, что если хотя бы одно из двух не будет соответствовать оригиналу, то приложение уже будет совершенно другим для системы Android.
                                      0
                                      На самом деле, подобная брешь не является виною только Android.

                                      Я не вижу тут бреши или неожиданного поведения системы.

                                      1) Сперва установим [переподписанную нами] игру 2048.
                                      2) Устанавливаем патч
                                      3) Переустанавливаем нашу [исходную переподписанную] игру

                                      Зачем нужен первый пункт? Достаточно запустить свою программу, которая формирует данные нужным образом, поверх накатить переподписанную с тем же ключом целевую программу.

                                      Именно поэтому следует создать патч для этого приложения, который был бы доступен широкой аудитории.

                                      Почему это называется «патчем»? Оно не меняет функционал исходной программы, меняет лишь её данные.
                                        0
                                        Боюсь, вы слишком многословны.

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое