Найти в Дзене

Немного реверса или как напоить сапера.

Не думаю, что кто-то когда-то задумывался о том, чтобы сделать что-то подобное, но был тихий вечер и мне было скучно. Тогда я задумался, а почему бы не сделать трейнер для Сапера? Вооружившись стареньким ноутбуком с Windows 7 на борту, я приступил к работе. Для начала надо было выяснить, что под капотам у этой программы. Для этого я использовал дизассемблер IDA Freeware. Открыв игру в дизассемблере, первым делом я пробежался по названиям функций и практически сразу наткнулся на функцию placeMines(int,int). "Вот оно!" подумал я и начал ее ковырять. В ней все происходило стандартно: сначала создавался массив в который по порядку помещались номера ячеек поля (с какой целью это делалось разбираться не стал), а потом уже отрабатывала функция случайных чисел, призванная передать случайный номер ячейки в функцию Array<NodeType>::Add(NodeType), которая и формирует массив с номерами "заминированных" ячеек. После недолгих размышлений я пришел к выводу, что лучший способ повлиять на расстанов

Не думаю, что кто-то когда-то задумывался о том, чтобы сделать что-то подобное, но был тихий вечер и мне было скучно. Тогда я задумался, а почему бы не сделать трейнер для Сапера? Вооружившись стареньким ноутбуком с Windows 7 на борту, я приступил к работе.

Сапер, вид сверху.
Сапер, вид сверху.

Для начала надо было выяснить, что под капотам у этой программы. Для этого я использовал дизассемблер IDA Freeware.

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

Названия функций удачно сохранились.
Названия функций удачно сохранились.

"Вот оно!" подумал я и начал ее ковырять. В ней все происходило стандартно: сначала создавался массив в который по порядку помещались номера ячеек поля (с какой целью это делалось разбираться не стал), а потом уже отрабатывала функция случайных чисел, призванная передать случайный номер ячейки в функцию Array<NodeType>::Add(NodeType), которая и формирует массив с номерами "заминированных" ячеек.

Блок функции placeMines(int,int), ответственный за расстановку мин в игре.
Блок функции placeMines(int,int), ответственный за расстановку мин в игре.

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

Код функции Array<NodeType>::Add(NodeType), добавляющий в массив случайное число.
Код функции Array<NodeType>::Add(NodeType), добавляющий в массив случайное число.

Итак, я понял как игра расставляет мины на игровом поле. Дело за малым. Вооружившись Cheat Engine и найдя нужную мне функцию, я начал создание скрипта, который бы подменял числа при добавлении в массив.

Код функции Array<NodeType>::Add(NodeType) в Cheat Engine.
Код функции Array<NodeType>::Add(NodeType) в Cheat Engine.
Делаем скрипт с помощью CheatEngine.
Делаем скрипт с помощью CheatEngine.

И вот, собственно, что получилось:

Скрипт.
Скрипт.

Изначально скрипт состоял из 4 строк кода:

mov [rax+rcx*4],ecx - здесь мы помещаем в область памяти, выделенной для массива, значение регистра ecx (он является счетчиком и начинается с 0).

add [rax+rcx*4],#3 - увеличиваем значение на 3 ( просто чтоб освободить первые три клетки). Интересный факт, что в игре первые две клетки никогда не задействуются под мины, видимо потому, что чаще всего люди в начале игры жмут именно их:)

mov eax,[rbx] - эта команда была в оригинальном коде, по этому ее я оставил не тронутой.

jmp exit - ну и команда перехода для возврата в вызывающую функцию.

Но потом выяснилось, что функция Array<NodeType>::Add(NodeType) очень уж популярна в MineSweeper и вышеуказанный код крашил игру на этапе запуска. Поэтому, немного поразмыслив, я добавил проверку на то, что именно функция placeMines(int,int) вызвала Add(NodeType):

cmp [rsp+28], "MineSweeper.exe"+27724

Суть этой проверки заключается в том, что мы смотрим адрес возврата в стеке и сравниваем его с адресом следующей команды после вызова Add(NodeType) в вызывающей функции placeMines(int,int) и если они совпадают, то можно утверждать, что мы на верном пути и можем исполнять наш код, если нет - то нет. Итог был положителен, сапер был пьян и просто положил мины в ряд:

Победа!
Победа!

Но на этом не все. Через CheatEngine конечно весело делать такие, но хотелось испытать свои силы и я решил перенести скрипт на чистый C++ (сразу скажу - на звание чистый и красивый код не претендую, все делалось из-за любопытства в качестве эксперимента и да, я не разработчик). Собственно, что делать в памяти выделенной для игры уже понятно:

1) выделяем себе динамическую память в процессе MineSweeper.exe;

2) в выделенной памяти вписываем наш код с проверкой адреса возврата, подменой значений в массиве и с точкой возврата;

3) в функции процесса заменяем несколько строк кода на адрес перехода к выделенной нами памяти (в соответствии с длинной опкода).

И вот я приступил к написанию тренера на C++ для сапера.

Первым делом в программе я удостоверялся, что игра запущенна, для этого просто копипастнул функцию из интернета подточив ее под себя.

Функция поиска и получения PID процесса.
Функция поиска и получения PID процесса.

После чего, открыв этот процесс, начинал творить жуткую жуть.

Начало функции, отвечающей за выделение памяти и вставки кода (да-да, все в одной фунции)
Начало функции, отвечающей за выделение памяти и вставки кода (да-да, все в одной фунции)

Байт-коды для полезной нагрузки я просто копировал из модуля редактора памяти CheatEngine, с тем расчетом, что менять там надо только адреса перехода и возврата. И тут началось самое интересное. Я долго не мог понять, почему при вычислении адреса перехода к моей выделенной памяти получалось отрицательное число. Причем данные поступали правильные, но в код писалось именно отрицательное число указывающее на неразмеченную область памяти. Вся проблема оказалось в функции VirtualAllocEx, которая, как я полагал, должна была выделять память в ближайших регионах от процесса (как это делает CheatEngine), но память ею выделялась не в первых свободных областях, а где-то (возможно я чего-то не знаю, не обессудьте). В итоге, я совершил бяку и просто циклически стал искать свободную память в процессе, начиная с базового адреса процесса.

Плохой код.... но он сработал.
Плохой код.... но он сработал.

После того, как память была найдена дело, вроде, было за малым. Много проблем я встретил с типизацией некоторых переменных и сложностью приведения их к определенному типу, по этому в моем коде встречается такой ужас:

Зато работает)
Зато работает)

Очень многие адреса хранились в переменных тип HANDL и их приведение к типу int_64 делал крааааааайне рискованным способом с использованием memcpy. Потом небольшие вычисления адресов типа такого "address = address + 0x27E69;" или такого "jmpToBack = address-allocated-sizeof(payload)+5;" (последняя цифра 5 это длина опкода перехода к нашей памяти, потому что вернуться мы должны именно ктой команде, которая идет следующей за нашим опкодом).

Также, у меня встречается такой код:

Опять рабочие циклы.
Опять рабочие циклы.

Он отвечает за вставку вычисленных адресов перехода в полезную нагрузку в определенных ее местах массива байт-кода.

И вот, скомпилировав все это, я получил свой маленький трейнер для игры "Сапер":

-14

Возможно для большинства это покажется бессмысленной тратой времени, но мне действительно было интересно, как работает тот же CheatEngine и смогу ли я сам, своими руками, с помощью C++ сделать то, что делает он. Это был очень интересный вызов самому себе в ходе которого получилось поглубже понять механизмы работы процессов, некоторые нюансы адресации и то, что эти знания можно использовать для еще более интересных вещей.