Не думаю, что кто-то когда-то задумывался о том, чтобы сделать что-то подобное, но был тихий вечер и мне было скучно. Тогда я задумался, а почему бы не сделать трейнер для Сапера? Вооружившись стареньким ноутбуком с Windows 7 на борту, я приступил к работе.
Для начала надо было выяснить, что под капотам у этой программы. Для этого я использовал дизассемблер IDA Freeware.
Открыв игру в дизассемблере, первым делом я пробежался по названиям функций и практически сразу наткнулся на функцию placeMines(int,int).
"Вот оно!" подумал я и начал ее ковырять. В ней все происходило стандартно: сначала создавался массив в который по порядку помещались номера ячеек поля (с какой целью это делалось разбираться не стал), а потом уже отрабатывала функция случайных чисел, призванная передать случайный номер ячейки в функцию Array<NodeType>::Add(NodeType), которая и формирует массив с номерами "заминированных" ячеек.
После недолгих размышлений я пришел к выводу, что лучший способ повлиять на расстановку мин это подменить случайные номера ячеек в момент их добавления в массив на нужные нам.
Итак, я понял как игра расставляет мины на игровом поле. Дело за малым. Вооружившись Cheat Engine и найдя нужную мне функцию, я начал создание скрипта, который бы подменял числа при добавлении в массив.
И вот, собственно, что получилось:
Изначально скрипт состоял из 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++ для сапера.
Первым делом в программе я удостоверялся, что игра запущенна, для этого просто копипастнул функцию из интернета подточив ее под себя.
После чего, открыв этот процесс, начинал творить жуткую жуть.
Байт-коды для полезной нагрузки я просто копировал из модуля редактора памяти CheatEngine, с тем расчетом, что менять там надо только адреса перехода и возврата. И тут началось самое интересное. Я долго не мог понять, почему при вычислении адреса перехода к моей выделенной памяти получалось отрицательное число. Причем данные поступали правильные, но в код писалось именно отрицательное число указывающее на неразмеченную область памяти. Вся проблема оказалось в функции VirtualAllocEx, которая, как я полагал, должна была выделять память в ближайших регионах от процесса (как это делает CheatEngine), но память ею выделялась не в первых свободных областях, а где-то (возможно я чего-то не знаю, не обессудьте). В итоге, я совершил бяку и просто циклически стал искать свободную память в процессе, начиная с базового адреса процесса.
После того, как память была найдена дело, вроде, было за малым. Много проблем я встретил с типизацией некоторых переменных и сложностью приведения их к определенному типу, по этому в моем коде встречается такой ужас:
Очень многие адреса хранились в переменных тип HANDL и их приведение к типу int_64 делал крааааааайне рискованным способом с использованием memcpy. Потом небольшие вычисления адресов типа такого "address = address + 0x27E69;" или такого "jmpToBack = address-allocated-sizeof(payload)+5;" (последняя цифра 5 это длина опкода перехода к нашей памяти, потому что вернуться мы должны именно ктой команде, которая идет следующей за нашим опкодом).
Также, у меня встречается такой код:
Он отвечает за вставку вычисленных адресов перехода в полезную нагрузку в определенных ее местах массива байт-кода.
И вот, скомпилировав все это, я получил свой маленький трейнер для игры "Сапер":
Возможно для большинства это покажется бессмысленной тратой времени, но мне действительно было интересно, как работает тот же CheatEngine и смогу ли я сам, своими руками, с помощью C++ сделать то, что делает он. Это был очень интересный вызов самому себе в ходе которого получилось поглубже понять механизмы работы процессов, некоторые нюансы адресации и то, что эти знания можно использовать для еще более интересных вещей.