Исследование игры-головоломки «Сапёр» (продолжение)

    Продолжаем наше исследование игры «Сапёр» от Microsoft.

    Данная статья является продолжением первой статьи.

    О чем будет идти речь:
    1) Взлом, основанный на переполнении буфера
    2) Взлом игровых мин
    3) Исследование архитектуры игры.

    I

    Откроем игру, следом за ней CE( Cheat Engine ). Присоединяемся к процессу игры:

    image



    и затем ищем значение открытых клеток поля в данный момент:



    далее ищем инструкцию( инструкции ), которые как-либо взаимодействуют с найденным адресом. Для этого добавляем найденный адрес в AddressList, кликаем правой кнопкой мыши и выбираем «Find out what accesses this address». Откроется дополнительное окно. Переходим в игру и жмем на какую-нибудь клетку:



    обнаружится, что с адресом взаимодействуют 3 инструкции( первая и третья схожи, но адреса различны ). В прошлый раз, как некоторые помнят, нас интересовали как раз те инструкции, которые писали в память напрямую( ASM-командой «mov» ), т.е первая и третья, но в этот раз мы сосредоточим внимание на второй инструкции, которая так же как и остальные, пишет в память, но немного другим образом. Она инкрементирует некоторое значение в памяти столько раз, сколько клеток поля было открыто( в данном случае это одна клетка ).
    Ок, эта инструкция вполне подойдет для того, чтобы переполнить буфер, в котором хранится количество открытых клеток поля в данный момент. Если посмотрим в дебаггере, как работает инструкция, то сразу обратим внимание на такую последовательность команд:



    сначала может показаться, что эта цепочка как-то связана с проверкой «нутра» клетки, т.е определение, ткнул ли игрок на клетку с миной. Скажу, что ничего подобного здесь не наблюдается. Вы можете сами это проверить, поставив BreakPoint на первое сравнение, например. Тогда при клике на обычную клетку, он сработает, но зато не сработает при клике на клетку с миной. Из этого следует, что определение внутренности клетки определяется где-то в другом месте.

    В таком случае, интересной здесь является лишь команда инкремента:

    inc [rcx+18]

    Пробуем изменить данную команду так, чтобы при открытии новой клетки поля прибавлялась не единица, а, например, максимальное целое число типа INTEGER, т.е 4294967295 ( HEX = FFFFFFFF ):



    Затем переходим в игру и начинаем «саперить»( для наглядности можно начать новую игру и выбрать поле побольше ):



    Как можно наблюдать, переполнение буфера работает в нашу пользу, но не в полной мере. При первом же клике по полю, сразу же открылось более половины всех клеток( в данном случае это более 128 клеток, как видно ). Играя дальше мы все равно сможем наткнуться на мину и «взорваться». При проигрыше игры и последующей демонстрации всех мин, мы заметим, что вместе с той половиной отсеявшихся клеток, отсеялась и половина всех мин. Это, конечно же облегчает прохождение игры, но не полностью — чуть более, чем на половину.

    II

    Теперь рассмотрим каким же образом можно «вручную» взаимодействовать с минами.

    Для этого перезапускаем игру( закрыть игру, затем открыть ). И снова аттачимся к ее процессу из CE. Для того, чтобы найти инструкцию, взаимодействующую с количеством мин в игре, необходимо сперва найти адрес в памяти, по которому хранится целое значение количества мин. Общий алгоритм:

    Поиск адреса памяти с количеством мин -> Поиск и исследование инструкций, работающих со значением -> отладка

    1) Ищем текущее количество мин в памяти. Для этого необходимо проделать алгоритм по поиску значения схожий с тем, который использовался для поиска текущего количества открытых клеток поля игры. Найти адрес достаточно просто в 2-3 поиска: сначала выбираем уровень сложности «Новичок»(10 мин) и ищем число 10 в памяти, затем меняем уровень сложности, например, на «Профессионал»( 99 мин ) и ищем в памяти число 99. Повторив это несколько раз, получится примерно следующее:



    Один адрес. Добавляем его в AddressList и смотри, что с ним взаимодействует. Для этого перезапустим( F2 ) игру:



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



    Хм, весьма интересно… здесь происходит инициализация многих значений. Есть теория, что мы угодили в функцию, срабатывающей при самом старте игры, которая занимается инициализацией всех внутриигровых значений. Выделим текущую функцию( правый клик по инструкции, в меню выбираем «Select current function» ):



    Функция довольно небольшая. Если прокрутить немного ниже, то это можно заметить. Ок, попробуем поставить бряк в начало функции:



    Затем переходим в игру и начинаем новую игру( F2 ). Сработает бряк, следовательно данная функция выполняется при начале новой игры. Теория подтверждена! Теперь возвращаемся к инструкции «mov», связанной с количеством мин. Инструкцию можно отдебажить, изменив её второй параметр — значение, записываемое по адресу в то место, где находится текущее количество мин:



    В данном случае произошла замена числа мин, соответствующее текущему уровню сложности, на четверку. Иными словами, мы изменили количество мин на 4. Переходим в игру, начинаем новую игру:



    Как видим, количество мин изменилось. Играем дальше:



    Классно :) Проблема с минами решена.

    III

    Завершающим этапом будет изучение архитектуры программного кода игры. Для начала давайте представим архитектуру любого стандартного приложения. Получится примерно следующее:

    1) Подключение библиотек, используемых в программе
    2) Описания классов и реализация их методов
    3) Точка входа. (В си, например, это функция main() )

    Собственно, как ни удивительно, по такому же принципу построено приложение «Сапер».
    Рассмотрим псевдокод, характеризующий внутреннюю архитектуру игры:

    class Application{...} // класс приложения
    class Game{

    ***

    void gameStart(){...}
    void gameLose(){...}
    void gameWin(){...}

    void createField(){}

    void Init(params){… }

    ***

    }

    int Main()
    {
    Game game;
    game.gameStart();
    }


    Как вы помните, во II пункте данной статьи мы отыскали функцию, которая выполняется при старте игры. Можно предположить, что ее реальный код находится недалеко от начала главной функции Main() . Тогда где-то выше должен находиться исходный код классов.

    Теперь же исследуем это под дизассемблером. Немного выше функции, найденной нами ранее, мы заметим такую вещь:



    Эти комментарии могут значить лишь одно — определенные логические блоки, выполняющие свои функции. Это не обязательно может быть архитектура игрового класса «Game». Это также могут быть обычные функции( языка более высокоуровневого ). Прочитав комментарии, можно легко понять, за что отвечает тот или иной логический блок данных.

    Так, например, несложно понять, что блок [RandSeed] отвечает за генерацию псевдослучайного числа, которое затем будет использоваться для задания случайных координат мин. Блок [Mines] служит для генерации случайных координат мин. И так далее.

    Вот, собственно, и все, что касается игры «Сапёр».
    В дальнейшем будут исследоваться другие игры, но уже более серьезные и более сложного уровня :)
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 22
    • +6
      Кат-кат-кат
      • 0
        А в чем же основная проблема статьи? Просто я не понимаю пока что вашего жаргона «кат-кат». Хотелось бы исправить это так, чтобы все было, как положено.
        • 0
          Хабракат: <habracut />. Вставить после первой картинки. Чтобы не разуплотняло главную страницу.
          • +2
            Основная проблема статьи в том, что она целиком на главной )
            • +2
              Ясно, спасибо. Не знал, что так получилось. Исправил.
        • 0
          Теперь можно ставить на огромное поле 1 мину и с первого раза пытаться ее поймать. Получится снайпер.
          • 0
            Только что узнал, что этого сделать нельзя. Так задумано :)
            • 0
              Так кто мешает, «Исследование игры-головоломки «Сапёр» (часть 3): отключение функции автоперемещения мины на первом клике. Теперь можно почувствовать себя настоящим сапёром взорваться сразу!»
              Asen, это намёк!
              • 0
                А мина там именно перемещается после клика? А не всё поле генерируется после того, как игрок кликнул по первой клетке.
                • +1
                  В XP-версии перемещалась, я когда-то находил адреса, по которым хранится собственно игровое поле, и это было отчетливо видно.
          • 0
            Не знал о существовании программы Cheat Engine, а благодаря вашим статьям узнал — спасибо!
            • +9
              inc [rcx+18]

              Пробуем изменить данную команду так, чтобы при открытии новой клетки поля прибавлялась не единица, а, например, максимальное целое число типа INTEGER, т.е 4294967295 ( HEX = FFFFFFFF ):

              Как можно наблюдать, переполнение буфера работает в нашу пользу, но не в полной мере

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

              Для начала давайте представим архитектуру любого стандартного приложения. Получится примерно следующее:

              1) Подключение библиотек, используемых в программе
              2) Описания классов и реализация их методов
              3) Точка входа. (В си, например, это функция main() )

              Давайте сначала представим архитектуру любого стандартного города. Получится примерно следующее:

              1) Завоз материалов для строительства
              2) Чертежи домов
              3) Вокзал (в Питере, например, это Московский вокзал).
              • 0
                Переполнение целочисленного буфера изображено на слайде, который тремя ниже того, который вы привели. Под «странным значением» вы подразумеваете максимальное целое число типа INT?
                • +1
                  Переполнение целочисленного буфера изображено на слайде

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

                  максимальное целое число типа INT?

                  Я уж не знаю, что вы называете типом INT, когда у вас 64-битная система, а дизассемблер не пишет ни dword ptr ни qword ptr.
                  Мне сначала показалось, что система ваша 32-битная, но тогда прибавление 0xffffffff равносильно вычитанию единицы.
                  А так, ну число какое-то, большое…
              • +2
                При чём здесь переполнение буфера?
                • 0
                  Просто фраза красивая, видимо.
                  • 0
                    Переполнение целочисленного буфера. Иначе же, что это по вашему? Где-то в игре присутствует переменная-счетчик типа INTEGER, которую мы пытаемся переполнить.
                    • +2
                      Я вам выше привёл ссылку на то, что обычно называют переполнением буфера.
                      В двух словах, переполнение буфера — это непреднамеренная запись программой данных в свою память. Непреднамеренная, потому что программа намеревалась записать данные в определённый массив/буфер, но в силу ошибок обработки адресов/индексов массива промахнулась мимо целевого буфера и переписала другие данные.

                      Целочисленные переполнения — это отдельный класс проблем, он может быть связан с буферами, а может быть не связан с ними. Вот я думаю, что если бы сапёр пытался писать в память, адрес которой отличается на 4Г от того, что он ожидал, он бы грохнулся с сегфолтом, а поскольку этого не произошло я и высказал сомнение, что имеет место переполнение буфера.
                  • 0
                    Вообщето все результаты сапера хранятся в обычном текстовом файлике, который можно поправить руками и вуаля вы открыли все мины за 1 сек. Помню когда то мой отец(любитель этой игры), долго возмащался что я задень побил его годовой рекорд))
                    • 0
                      Объясните лучше, как в подобных ситуациях быть?
                      Скриншоты
                      Речь о правом нижнем угле.
                      image
                      image
                      • 0
                        Если вы использовали «переполнение буфера», то, как я уже говорил, данный способ не может гаранировать 100% победы. Вероятность победы прямопропорциональна количеству открытых клеток при первом клике по полю. Т.е чем больше клеток удалось открыть, тем большая вероятность отсеивания большего количества мин. В данном случае отсеивается не менее 50% клеток поля, а вместе с ними и 50% всех мин( об этом сказано с статье и видно из вашего скрина ). Далее же придется полагаться на свой опыт в игре и удачу, так как способ, основанный на переполнении буфера, лишь упрощает процесс игры, а не полностью его решает :)
                      • 0
                        Вы молодец, что занялись подобного рода развлечениями. Копаться в дизассемблированном коде игр приятно и, иногда, небесполезно. Читать об этом тоже занятно, тем более, что пишите вы интересно. Продолжайте изыскания, пишите еще.

                        Теперь о том что плохо.
                        В первой части вашей статьи написана ерунда. Код, на который вы обратили внимание (inc[rcx + 0x18]), действительно подсчитывает количество открытых клеток. Но, во-первых, когда вы заменяете его на (add[rcx + 0x18], 0xFFFFFFFF) не происходит никакого переполнения буфера, просто вместо прибавления единички, она отнимается. Зато, благодаря тому, что вы варварски затираете две инструкции, (xor edx, edx и mov r13, r10) происходит небольшой побочный эффект. Кстати, попробуйте, убрать только mov r13, r10, тоже интересно получается. Во-вторых, этот счетчик открытых клеток служит только для одного: определять когда игра закончилась. Это происходит так:
                        (if (board->width) * (board->height) == board->openedFields + board->numMines) {
                        board->gameWin();
                        }
                        так что, изменяя эту ячейку памяти, вы абсолютно ни на что не влияете.

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