Pull to refresh

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

Reading time4 min
Views15K
Продолжаем наше исследование игры «Сапёр» от 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] служит для генерации случайных координат мин. И так далее.

Вот, собственно, и все, что касается игры «Сапёр».
В дальнейшем будут исследоваться другие игры, но уже более серьезные и более сложного уровня :)
Tags:
Hubs:
+19
Comments22

Articles

Change theme settings