Туториал по написанию трейнеров для DMA и не-DMA игр
Содержание:
1. Немного теории
2. Не-DMA игры
3. DMA игры - поиск поинтера
4. Пишем трейнер для:
4.1 Трейнер для не-DMA игры
4.2 Трейнер для DMA игры
4.3 Общие замечания
5. Заключение
6. Некоторые оговорки
7. Контакты и ссылки
8. Условия распространения и всё прочее
В этой статье я рассмотрю написание трейнера (trainer'а) для игры. В рунете совсем мало информации на эту тему, а про написание DMA-трейнеров я вообще ничего не нашёл (может быть, конечно, плохо искал), но в процессе написания мною была дана торжественная клятва, что когда закончу - обязательно напишу нормальный туториал на эту тему. Хотелось написать туториал, в котором бы затрагивалось в равной степени как теория, так и поиск поинтера и написание собственно трейнера.
Основную часть я намерен посвятить именно DMA играм.
Итак, что потребуется:
● - Язык программирования. Я буду использовать Delphi для примеров. В принципе, подойдёт любой - для работы с памятью процесса нам потребуются только WinAPI функции.
● - Программа типа ArtMoney (чтобы искать значения в памяти). Настоятельным образом рекомендую TSearch (несмотря даже на то, что весит он полтора метра), буду использовать его в примерах.
● - Отладчик. Опять же рекомендую TSearch - он содержит в себе простой и удобный отладчик, которого вполне хватит. Если у вас есть SoftICE, и вы умеете им пользоваться - то флаг в руки.
● - Минимальные знания ассемблера, общее (хотя бы теоретическое) представление об отладке программ, устройстве памяти.
приступим...
1. Немного теории:
Прежде всего, что такое DMA? DMA - dynamic memory allocation, т.е. динамическое распределение памяти. Проще говоря, DMA игры, в отличие от не-DMA игр, хранят используемые ими величины (нас будут интересовать деньги, жизни и т.п.) по адресам в памяти, которые меняются после каждого запуска/перезапуска/загрузки игры.
Все программы DOS не используют DMA, тогда как большинство игр под Win32 его использует (не используют только игры времён Win95-Win98). С не-DMA играми всё предельно просто - нужно просто найти адрес в памяти, где игра хранит интересующее нас значение, и изменить его. А вот с DMA могут сложнее - что делать, если адреса постоянно изменяются? Для того, чтобы ответить на этот вопрос, нужно понять, как сама игра находит нужный адрес. Для этого используются поинтеры (pointer - указатель, я буду их называть и так, и так). Адреса поинтеров, в отличие ото всех остальных, не изменяются. Поинтер содержит значение, которое соответствует адресу какой-то величины, используемой игрой. Не важно какой, важно, что смещение других адресов относительно адреса, на который указывает поинтер, также не меняется (*). (Если не очень понятно - уверен, что станет понятнее в практической части) Итак, трейнер для DMA игры будет сначала считывать из указателя адрес интересующего значения, а затем уже его изменять. Осталось только его найти и написать соответствующую программу. (Всего-то делов, да? )
Про регистры процессора, про то, как устроен стек и т.д. я рассказывать не намерен. Для этого есть учебники по ассемблеру (вообще говоря, эти темы так или иначе затрагиваются в любом учебнике программирования на любом языке).
Теперь к практике...
2. Не-DMA игры.
Как я уже говорил, тут всё предельно просто. Запускаем ArtMoney/TSearch/GameHack и т.п., ищем, затем отсеиваем, определяем адрес интересующего значения. Пишем трейнер в пункте 4.1.
3. DMA игры - поиск поинтера.
Итак, сначала нужно найти адрес интересующей величины. Теперь нужно поставить брейкпоинт на этот адрес (TSearch - в меню: AutoHack -> Enable Debugger, Enable AutoHack window, там нажимаем на кнопку добавления брейкпоинта и вводим адрес нашей величины). Мы ставим брейкпоинт на чтение/запись этого адреса, т.е. при чтении или записи этого адреса нам будет показано, какие инструкции в программе и в каком месте читали/писали из/в него (в более профессиональных отладчиках, например, SoftICE, выполнение всех программ приостановится, и вылезет окно отладчика). Далее нужно переключиться в игру и изменить величину. После того, как он изменится, в окне отладчика TSearch появится строка, например, mov [ebx+A], eax (A - некоторое смещение, может быть любым целым числом, например 4). Эта ассемблерная инструкция устанавливает значение по адресу ebx+A равным eax. Что такое ebx+A? Это и есть наш адрес, +A - смещение относительно ближайшего указателя. То есть, поинтер указывает на какой-то адрес, а через A от него находится интересующее нас значение, и это значение всегда будет смещено на A относительно адреса, на который указывает поинтер. Но мы ещё не знаем адрес поинтера (адрес у нас был в регистре ebx, но он должен быть где-то в памяти). Искать его придётся как и любое другое числовое значение. Вычтем из адреса интересующего нас значения, который мы нашли в самом начале, A, затем переведём его в десятеричную систему счисления, а затем будем искать. Возможно, мы найдём несколько адресов, содержащий такое число, но совсем не факт, что все эти адреса - адреса указателей. Для этого придётся перезагрузить игру, затем вновь найти интересующее значение, из его адреса вычесть A, перевести в десятеричную систему счисления, и отсеивать.
Слова, конечно, хорошо, но на примерах любое изучение идёт лучше.
Пример: Red Alert 2. Для наглядности буду использовать TSearch. Запускаем. Open Process - выбираем нужный процесс (game.exe), приступаем к поиску: деньги - Exact Value, 4 bytes. Меняем количество денег. Отсеиваем. В результате останется 3 значения, но только одно из них - значение собственно денег, остальные два - значения счётчика и ещё что-то. (Я знаю, что значение с самым большим адресом - то, которое нужно) У меня этот адрес получился равным 72C6DAC. Далее устанавливаем на этот адрес брейкпоинт. Изменяем значение. В окошке отладчика видим: 4E48FF:
mov [ebx+0x24C], eax. Ассемблерная инструкция mov [ebx+0x24C], eax устанавливает значение, равное eax по адресу ebx+024C (ebx - регистр процессора, содержащий адрес, квадратные скобки указывают на то, что число, содержащееся в них, - адрес, и нужно изменять значение по этому адресу), но нас интересует только ebx+0x24C. Отнимаем от 72C6DAC (адреса, по которому хранится значение денег) 24Ch - получаем 72C6B60, переводим в десятеричную систему счисления - 120351584. Теперь ищем это число в памяти. В начале я получил 37 адресов. Теперь по новой ищем адрес, по которому находятся деньги, не забыв предварительно добавить в таблицу уже найденные адреса потенциальных указателей. На этот раз это 73B29FC. Вновь отнимаем 24С, переводим, ищем. В результате у меня осталось несколько адресов. Пожалуй, можно выбрать любой. Один из них - A1E0C4, сгодится. Перезапускаем. Итак, прибавим к значению, хранящемуся по адресу A1E0C4, 24С. Теперь переводим результат сложения в шестнадцатеричную систему счисления и смотрим на значение по этому адресу. Если мы видим там количество наших денег - значит всё удалось, поздравляю (Если нет - придётся повторить всё с начала)
Итак, поинтер мы нашли. Пожалуй, это был самый сложный этап.
4. Пишем трейнер.
Для записи и чтения в памяти мы будем использовать две WinAPI функции - ReadProcessMemory и WriteProcessMemory. (Всё предельно просто)
4.1. Трейнер для не-DMA игры.
К примеру, возьмём старую DOS-овскую игрушку Raptor: Call of The Shadows.
var
Form1: TForm1;
WindowName: integer; // Для удобства объявим как глобальные переменные.
ProcessId: integer; // Все эти переменные нужны для того, чтобы найти
ThreadId: integer; // процесс с игрой.
HandleWindow: Integer; //
write: cardinal; // В эту переменную попадёт количество записанных байтов.
buf: dword; // Тут будет содержаться значение, на которое будем изменять.
const
WindowTitle = 'RAP'; // Заголовок окна с игрой
Address = $83C4BF64; // Адрес, по которому будем изменять значение.
NumberOfBytes = 4; // Количество байт, которые будем заменять.
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
WindowName := FindWindow(nil,WindowTitle);
If WindowName = 0 then begin // Если окошка у нас нет, то и изменять нечего.
MessageDlg('Игра должна быть запущена до трейнера. Запустите ее, потом трейнер', mtwarning,[mbOK],0);
end;
ThreadId := GetWindowThreadProcessId(WindowName,@ProcessId); // Ищем хэндл процесса
HandleWindow := OpenProcess(PROCESS_ALL_ACCESS,False,ProcessId); // с нашей игрой.
buf:=$DEAD; // DEADh = 57005d
WriteProcessMemory(HandleWindow, ptr(address), @buf, 4, write); // Изменяем значение по этому адресу на наше.
end;
4.2. Пишем трейнер для DMA-игры.
Тут уже будем читать из поинтера адрес, по которому будем далее менять значение. Возьмём в качестве примера, скажем, SimCity 4.
var
Form1: TForm1;
WindowName : integer;
ProcessId : integer;
ThreadId : integer;
HandleWindow : Integer;
b:dword; // Всё по-прежнему, кроме этой переменной - сюда прочитаем адрес из поинтера.
readwrite:cardinal;
buf : dword;
Const WindowTitle = 'SimCity 4';
Address = $B321E4; // Это адрес поинтера.
NumberOfBytes = 4;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
WindowName := FindWindow(nil,WindowTitle);
If WindowName = 0 then
begin
MessageDlg('Игра должна быть запущена до трейнера. Запустите ее, потом трейнер', mtwarning,[mbOK],0);
end;
ThreadId := GetWindowThreadProcessId(WindowName,@ProcessId);
HandleWindow := OpenProcess(PROCESS_ALL_ACCESS,False,ProcessId);
ReadProcessMemory(HandleWindow,ptr(address),@b,4,readwrite); // Прочитали в b значение из адреса поинтера.
b:=b+40; // Смещение адреса денег относительно адреса, на который указывает поинтер равно 40. Прибавляем.
buf:=$FFFFFFFF; // Денег должно быть много (**)
WriteProcessMemory(HandleWindow, ptr(b), @buf, 4, readwrite); // Наконец, запишем по адресу,
// содержащемуся в b, новое значение денег.
end;
Для того, чтобы значение "заморозить" нужно выложить на форму таймер или использовать бесконечный цикл (думаю, это итак понятно ), но в этом случае будет очень полезно проверять значение по адресу в b, т.к. всё в том же SimCity 4 если выйти со включённым таймером на экран выбора города - игра вывалится.
Также хорошей идеей является использование горячих клавиш. В тему борьбы с DMA это не входит, так что всё это на вкус читателя.
4.3. Общие замечания
Хочется ещё обратить внимание на то, сколько мы байт читаем и записываем (константа NumberOfBytes). Есличитаем из поинтера - то читать нужно 4 байта (т.е. dword - переменная, в которую читаем, должна уместить в себя это значение; все адреса - 32-разрядные). Если пишем значение, размер которого 1 байт - то соответственно и писать надо 1 байт Иначе, опять же, чревато аварийным завершением игры.
Напоминаю, что:
byte это 1 байт (это 8 бит ) - число от 0 до 255.
word это 2 байта - число от 0 до 65535.
dword это 4 байта - число от 0 до 4294967295.
Приводить описание всех использованных WinAPI функций я счёл ненужным, так что если интересно - смотри MSDN сам.
5. Заключение.
Ну вот и всё. Трейнер написан и работает (по крайней мере, надеюсь на это), остаётся лишь пожелать всем удачи в этом непростом деле
Удачи, Skversh.
6. Некоторые оговорки
* - я пишу, что смещение адресов относительно поинтера не меняется. Это так. Но иногда его сложнее найти - встречаются конструкции типа mov [ebx+ecx], eax (правда, встречается значительно реже - это всякие полоски с жизнью и другие неявные величины). Тут хорошо бы вооружиться отладчиком посерьёзнее (SoftICE) и либо смотреть, что откуда попадает в регистры (быть указаны явно), либо ставить брейкпоинт на адрес, когда брейкпоинт сработает - смотреть содержимое регистров и искать их в памяти по-отдельности, точно так же, как мы это делали в случае с одним регистром. Но так или иначе, раз игра находит адрес - то можем найти и мы. (Это здесь написано для того, чтобы не возникало сомнений относительно неизменности адреса поинтера 8) )
** - я смело заменяю значение на FFFFFFFFh. Это прокатит для SimCity 4, но в общем случае также чревато последствиями. (Значение может быть и 4 байта, но такая цифра может не влезть в строку, вылезти за пределы экрана или ещё как-нибудь заглючить , в общем, лучше не жадничать)