WarCraft 3: Часть I: Отслеживаем мышь

WarCraft: Взгляд изнутри

Акт 1: Запуск War и «отлов» событий

Начнём с того, что напишем программу, запускающую War и отслеживающую некоторые события, происходящие внутри него. (Готовый пример лежит в каталоге Ex1 архива exs.zip). Ни для кого не секрет, что сам «движок» War’а заводится при запуске файла war3.exe. Все остальные экзешники – это лишь обёртки, выбирающие режим его работы. Если запустить просто war3.exe, то War запустится как TFT. На Delphi запуск War’а реализуется так:

 FillChar(sti,SizeOf(sti),0);     //забиваем нулями
 FillChar(pi,SizeOf(pi),0);
 sti.cb:=SizeOf(sti);             //размер структуры
 sti.dwFlags:=STARTF_USESHOWWINDOW;//флаги запуска
 sti.wShowWindow:=SW_SHOWNORMAL;
 if not CreateProcess('war3.exe',nil,nil,nil,false,
               DEBUG_PROCESS or DEBUG_ONLY_THIS_PROCESS,
               nil,nil,sti,pi) then begin
  MessageBox(0,'Ошибка запуска War',’Ошибка’,MB_ICONSTOP);
  exit;
 end;//of if
Итак, мы заполняем все необходимые структуры и пускаем War.exe вызовом CreateProcess. Обратите внимание на флаги DEBUG_PROCESS и DEBUG_ONLY_THIS_PROCESS. Ими мы указываем, что хотим запустить War «изнутри» нашей программы, иметь полный доступ к нему (иметь его по полной программе :)), и получать сообщения о некоторых событиях, происходящих в его недрах.
Кстати, такой способ контроля посторонней программы используется всеми отладчиками. И теперь наша программа получает возможность вызывать особые «отладочные» функции (Debug API) для управления запущенным процессом.
Далее мы просто ждём наступления какого-либо отладочного события:
WaitForDebugEvent(de,INFINITE)
Когда в недрах War’а произойдёт некоторое событие, функция вернёт его полное описание в структуре de. Проанализировав de.dwDebugEventCode, можно определить, что за событие мы получили. Их довольно много (см. Delphi HELP), но нас особо интересуют вот эти:
  • RIP_EVENT – отладка накрылась. Отлаживаемый процесс понял, что его отлаживают, и «вырубил» наш отладчик. К счастью, War не содержит никаких антиотладочных и антихакерских трюков, что сильно облегчит нам жизнь.
  • CREATE_PROCESS_DEBUG_EVENT – процесс создан. Мы получим такое событие, когда War загрузит все DLL’ки, создаст главный поток и будет готов к выполнению.
  • CREATE_THREAD_DEBUG_EVENT – War создал новый поток. Как известно, War – многопоточное приложение, поэтому такое событие будет происходить довольно часто.
  • EXIT_THREAD_DEBUG_EVENT – Какой-либо из потоков завершился (или завершён принудительно – как известно, War вырубает подвисшие потоки, что несколько затрудняет жизнь JASS’ерам).
  • EXIT_PROCESS_DEBUG_EVENT – War завершился (или вылетел, без разницы).
  • EXCEPTION_DEBUG_EVENT – в недрах War’а возникло исключение (некая особая ситуация). Например, такое событие мы получим при выполнении первой инструкции War’а, при срабатывании точек останова (установленных нами), при выполнении некоторых наперёд заданных условий, при возникновении критической ошибки War’а и т.д. Проанализировав de.Exception.ExceptionRecord.ExceptionCode можно установить, чем вызвано исключение.
Получив какое-то событие, мы можем его обработать (если хотим). Причём выполнение War’а временно приостанавливается, и чтобы он продолжил работу, необходимо вызвать функцию ContinueDebugEvent с аргументом DBG_CONTINUE. Особую проблему представляет обработка исключений. Если мы, обрабатывая исключение, вызовем ContinueDebugEvent с аргументом DBG_CONTINUE, то исключение тут же возникнет вновь (т.к. условия для его возникновения никуда не исчезли). И ещё, и ещё раз… В общем, дело так и не сдвинется с мёртвой точки. Чтобы рестартовать War после исключения, нужно использовать DBG_EXCEPTION_NOT_HANDLED.
Стоп, всё не так просто! Оказывается, существует ещё одно, совершенно особое исключение – EXCEPTION_BREAKPOINT. При его первом возникновении нужно использовать DBG_CONTINUE, а во все последующие разы – DBG_EXCEPTION_NOT_HANDLED.
Что ж, вся теория позади, переходим к практике. Напишем программу, которая будет отслеживать создание/удаление потоков War’а и выводить их количество в файл zz.txt. В общем, пускаем War, затем создаём наш лог:

CountOfThreads:=0;IsFirstRun:=true;
AssignFile(f,'zz.txt');
Rewrite(f);
Пока что количество потоков – 0 (War только-только запущен). Переменная IsFirstRun позволит нам отследить первое исключение типа EXCEPTION_BREAKPOINT, чтобы воспользоваться волшебной «гасилкой» DBG_CONTINUE.
Далее начинаем обрабатывать отладочные события. Обработка организована в виде цикла:

while WaitForDebugEvent(de,INFINITE) do begin
  case de.dwDebugEventCode of
Т.е. ждём событие/анализируем тип/снова ждём (это, конечно, только заголовок цикла – самое его начало). В зависимости от события, ведём себя по-разному:

RIP_EVENT:begin //Отладка накрылась
    CloseFile(f);
    MessageBox(0,'Ошибка внедрения в War','Ошибка',mb_iconstop);
    exit;
   end;//of RIP_EVENT
Да, если отладке крышка - выходим. То же самое – если War завершился (см. исходник – повторяться тут нет смысла). Если же создан поток, отметим этот факт в логе:

CREATE_THREAD_DEBUG_EVENT:begin //создан новый поток
    inc(CountOfThreads);           //увеличить на 1 счётчик потоков
    Write(f,'THR:');               //вывести эту информацию
    Writeln(f,CountOfThreads);
   end;//of CREATE_THREAD_DEBUG_EVENT
Аналогично отмечается и уничтожение потока (см. исходник).
Теперь перейдём к обработке исключений. Их мы тоже будем отмечать в логе – за исключением сакраментального EXCEPTION_BREAKPOINT:

   EXCEPTION_DEBUG_EVENT:with de.Exception.ExceptionRecord do begin
    //тип исключения - первая инструкция
    if (ExceptionCode=EXCEPTION_BREAKPOINT) and IsFirstRun then begin
     IsFirstRun:=false;           //сбросим флаг
     ContinueDebugEvent(de.dwProcessId,de.dwThreadId,DBG_CONTINUE);
    end else
     ContinueDebugEvent(de.dwProcessId,de.dwThreadId,DBG_EXCEPTION_NOT_HANDLED);
    //Обрабатываем только исключение отладки
    writeln(f,'Exc');
    continue;                     //перезапуск цикла
   end;//of EXCEPTION_DEBUG_EVENT
Как видите, основная проблема тут – выбрать, что передавать в ContinueDebugEvent.
Ну, вроде всё. Компилируем, пускаем (только не из-под Delphi! Он сам содержит отладчик и будет конфликтовать с нашей программой. Поэтому компилируем её по Ctrl+F9, копируем в папку с War’ом и запускаем). По окончании игры смотрим листинг. Основной результат такого исследования состоит в том, что War в момент запуска создаёт 19 потоков, а затем их только использует! Большинство потоков висят замороженными, и «размораживаются» по мере надобности. Взамен каждого подвисшего потока тотчас создаётся новый.
Кстати, на самом деле потоков 20, а не 19 – т.к. создание главного потока фиксируется событием CREATE_PROCESS_DEBUG_EVENT, а его мы не обрабатывали.

Акт 2: Ищем триггерную переменную

Теперь усложним нашу программу – сделаем так, чтобы она искала в недрах War’а нужную нам переменную и выводила в лог её адрес. Готовый исходник лежит в Ex2.
Прежде всего, цикл ожидания событий вынесен в отдельную процедуру - ProcessEvents. Там же содержится проверка – а не завершился ли War. И если завершился, выходим:

GetExitCodeProcess(pi.hProcess,ec);
if ec<>STILL_ACTIVE then halt(0);
Кроме того, события ожидаются не вечно, а в течение указанного (далее) времени – чтобы наша программа тоже периодически получала управление. Ещё одна деталь – теперь мы объявляем новый тип:

 type Thr=record
 handle,id:cardinal;
end;//of Thr
и массив:

WarThreads:array [1..100] of Thr;
В этом массиве мы будем сохранять хэндлы всех создаваемых War’ом потоков. Зачем? Ну, так ведь хэндл («регулятор») – волшебный ключик к потоку. Имея хэндл потока, мы можем иметь и сам поток (причём по полной). Так что при создании каждого потока его хэндл сохраняется в вышеуказанном массиве, а при уничтожении потока – удаляется из массива. Т.о., массив всегда будет содержать хэндлы всех действующих потоков. В этом примере (как и паре последующих) хэндлы нам не потребуются. Но лучше заранее позаботиться об их накоплении.
Итак, начало программы – стандартное: пускаем War, создаём лог-файл… А дальше выдерживаем 7-секундную паузу (ждём события):

for i:=1 to 7 do ProcessEvents(1000);
Т.е. ждём 7 раз по 1000мс, что в итоге даёт 7 сек. Почему нельзя было поставить ProcessEvents(7000) ? Ну, так ведь эта процедура – самопальная. Мне лень было её синхронизировать по таймеру, и чем меньше интервал, тем точнее он отрабатывается (парадокс, не правда ли?). Для чего нужна эта пауза? Дело в том, что War грузится не мгновенно, да и на загрузку карты какое-то время уходит. А пока War и карта грузятся, сканировать память нет смысла – нужной переменной там заведомо нет. За 7 сек. War успевает загрузиться, пользователь – выбрать карту, и сама карта уже на подходе. Самое время начать сканирование. Для него я создал особую процедуру – MemScan, которая ищет некоторую строку, начиная с указанного адреса. Разумеется, ищем с 0. Увы, War использует немыслимо большие объёмы памяти, так что полный поиск будет идти ну о-очень долго. А ведь нам нужно вести поиск регулярно – карту-то могут в любой момент загрузить. К счастью, есть способ ускорить дело – блочный поиск.
Прежде всего получаем информацию об очередном блоке памяти:

VirtualQueryEx(pi.hProcess,pointer(StartAddress),mbi,SizeOf(mbi));
Эта функция возвращает информацию об указанном блоке памяти. А далее большинство блоков просто отсеиваем. Во-первых, искать переменную в пустых (свободных) блоках памяти бессмысленно, как и в зарезервированных. Поэтому такие блоки пропускаем, ощутимо сэкономив на их сканировании:

  //Проверить, что в этой области лежит
  if ((mbi.State and MEM_FREE)<>0) or
     ((mbi.state and MEM_RESERVE)<>0) then begin //область пута
   StartAddress:=StartAddress+mbi.RegionSize;
   continue;                      //пропускаем эту область
  end;//of if
Далее анализируем флаги блока памяти. Если там хранится программный код, то этот блок опять-таки пропускаем. Сканируются только блоки данных:

  //ищем данные (доступ на запись)
  if ((mbi.AllocationProtect and 1)=0) or
     ((mbi.Type_9 and MEM_PRIVATE)=0) then begin
   StartAddress:=StartAddress+mbi.RegionSize;
   continue;
  end;//of if
Если же подходящий для сканирования блок найден, переводим его в текстовую строку и просматриваем инструкцией pos, которая работает очень быстро:

  //Подходящая для сканирования область найдена
  SetLength(dta,mbi.RegionSize);
  num:=0;
  ReadProcessMemory(pi.hProcess,mbi.BaseAddress,@dta[1],mbi.RegionSize,num);
  //Вариант: 98765432
  StartAddress:=StartAddress+mbi.RegionSize;
  ps:=pos(ss,dta);
  if ps<>0 then begin //найдено!
Вот так. Теперь осталось вычислить адрес найденной переменной (чтобы потом записывать туда нужные данные) и вывести параметры блока переменных в лог-файл. Кроме того, одновременно создаётся дамп памяти – т.е. содержимое всего блока переменных записывается в файл dmp.bin для дальнейшего изучения:

  if ps<>0 then begin
   Result:=ps+cardinal(mbi.BaseAddress)-1;
   writeln(f,'----');
   write(f,'ADR:');writeln(f,Result);
   write(f,'TYP:');writeln(f,mbi.Type_9);
   write(f,'AP:');writeln(f,mbi.AllocationProtect);
   write(f,'ST:');writeln(f,mbi.State);
   assignFile(fb,'dmp.bin');
   rewrite(fb,1);
   BlockWrite(fb,dta[1],length(dta));
   CloseFile(fb);
   if IsOne then IsOne:=false
   else exit;
  end;//of if
Вы, наверное, удивились – что это за странные манипуляции с флагом IsOne? Всё дело в том, что коварный War хранит переменные в двух экземплярах – в самой карте и в области данных скрипта. Причём нам нужна именно вторая копия, т.к. именно она «рабочая», тогда как первая используется для быстрого перезапуска карты (пункт War’овского меню «начать заново»). Поэтому первую найденную переменную мы спокойно пропускаем, а вот адрес второй уже используем.
Итак, процедура сканирования памяти готова, пауза 7 сек. выдержана, самое время приступить к сканированию! Оно будет проводиться каждые 2 сек (чтобы не занимать ресурсы) до нахождения переменной:

 //4. Каждые 2 секунды сканируем всю память
 while MemScan(0,#120#10#227#05)<100 do begin
  ProcessEvents(2000);
 end;
Маловразумительная строка #120#10#227#05 – это символьное представление числа 98765432, которое мы выбрали в качестве удобного шаблона для поиска (т.е. вероятность того, что War использует его ещё где-то, практически нулевая). Ну, а после нахождения переменной получаем её адрес и пишем его в лог:

i:=MemScan(0,#120#10#227#05);
writeln(f,i);
Итак, переменная найдена, дамп памяти снят, лог заполнен… Теперь осталось только дождаться выхода из War’а:

ProcessEvents(INFINITE);
Всё. Компилируем программу, пускаем её, грузим карту-пример (zzz.w3m), и затем может сразу выйти из игры. После этого в папке War’а оказывается лог примерно следующего содержания:

----
ADR:250880332
TYP:131072
AP:1
ST:4096
Отсюда видно, что War использует для своих данных защиту – вся память, используемая под данные, обозначается как NOACCESS, т.е. просто взять и записать туда что-то нельзя – это обеспечивает защиту от неправильно работающих скриптов. Впрочем, мы будем пользоваться функцией WriteProcessMemory, которая может записывать и в защищённые участки памяти ;).
Кроме того, в том же каталоге появится небольшой файл дампа области переменных (обычно не более 100Кб). Он обладает двоичной структурой, и анализировать его можно только в HEX-редакторе (я предпочитаю HexEd, т.к. он идёт в комплекте RadASM, небольшой по размеру и совершенно бесплатен).

Акт 3: Передаём данные в War

Открываем каталог Ex3 и смотрим исходник. От Ex2 он отличается только тем, что в найденную переменную мы пишем некое число:

j:=$00100010;
WriteProcessMemory(pi.hProcess,pointer(i),@j,4,tmp);
Команды записи стоят сразу после цикла поиска переменной. Если откомпилировать программу и запустить её, а потом загрузить карту-пример, то она выведет на экран “x=16,y=16”, а не тот мусор, что раньше. Т.е. контакт с War’ом уже есть! Мы можем передавать туда всё, что угодно.

Акт 4: Надёжный поиск

Анализ дампов памяти, созданных предыдущими примерами, показал, что каждая целочисленная переменная занимает целых 40 байт памяти, лишь 4 из которых содержат число. Ещё 8 указывают тип переменной. Например, целочисленный тип описывается так:
04 00 00 00 – 04 00 00 00 Т.е. теперь мы можем повысить надёжность поиска, не только сканируя память на указанное число (мало ли, где оно может встретиться?), но ещё и проверяя тип переменной. Кроме того, вы, вероятно, были в некотором недоумении – почему число 98765432 в виде строки представлено как #120#10#227#05 ? А если вам хочется использовать другой шаблон поиска, что тогда? Или если нужно записывать что-то сразу в несколько переменных, ведь им нельзя присваивать одинаковые значения… Поэтому открываем каталог Ex4 и смотрим. Новая программа отличается от Ex3 лишь небольшими изменениями процедуры MemScan. Теперь она принимает не строку, а сразу ЧИСЛО, которое нужно найти в памяти. Строку из него она формирует самостоятельно, да ещё и дополняет её типом переменной для более надёжного поиска. Для этого там прежде всего объявляется константная строка, содержащая тип переменной:

Const stp=#4#0#0#0#4#0#0#0;
Затем число (vr) переводится в строку и добавляется к строке типа:

s:=stp+'abcd';     
Move(vr,s[9],4);
Всё! Теперь сканировать память можно так:

adr:=MemScan(0,98765432)

Акт 5: Наконец-то добрались до курсора!

Ну, теперь уже можно приступить к отслеживанию курсора мыши (не прошло и полугода…). Готовый пример находится в каталоге Ex5. Прежде всего, убираем все строки кода, связанные с созданием и ведением логов (в самом деле, зачем они нам теперь-то?). И – после нахождения переменной каждые 40мс будем записывать туда позицию курсора мыши (в сжатой форме):

 repeat
  ProcessEvents(40);
  GetCursorPos(pt);
  j:=(pt.y shl 16)+pt.x;
  WriteProcessMemory(pi.hProcess,pointer(i),@j,4,tmp);
 until false;
Т.е. используется бесконечный цикл, который завершается только после завершения War’а. Запустите программу+карту… И увидите, как при движении курсора мыши меняются числа в табличке. Правда, триггер срабатывает довольно редко, так что числа меняются рывками. Но вы можете легко это исправить (в редакторе триггеров уменьшить интервал срабатывания).

Акт 5а: Это реально!

От целочисленных переменных переходим к реальному типу. Возможно, вам интересно, как можно находить «реальные» переменные и чего-либо в них записывать. Готовый пример находится в каталоге Ex5. Эта программа напоминает Ex3, т.е. ничего не отслеживает, а только находит переменную со значением 123.45 (удобный шаблон, не правда ли?) и заменяет её на 11.11. Прежде всего, анализ дампов памяти показал, что реальный тип в War’е обозначается как
05 00 00 00 – 05 00 00 00 и соответствует Delphi’шному типу single. Соответственно, он конвертируется в строку… И всё – аналогично предыдущим примерам. См. исходник. Да, кстати, карта zzz.w3m для тестирования этого примера не подходит, т.к. она пользуется только целочисленными переменными. Поэтому в том же каталоге находится карта real.w3m (содержит переменную с шаблоном и выводит на экран её значение).

Акт 6: Это только начало…

Вы думаете, всё так просто? Ха-ха! Как вы считаете, что произойдёт, если игрок выйдет в главное меню War’а или загрузит новую карту? Ведь мы каждые 40мс пишем координаты курсора в найденную переменную, а при выходе из карты освободившаяся память занимается War’ом под что-то другое. И не факт, что даже перезапуск карты оставит переменную на том же месте! Т.е. при перезапуске карты или выходе из неё War может та-ак глюкануть… Вывод: для безглючной работы программы жизненно необходимо отслеживать момент выгрузки карты и начинать сканирование сызнова.
Это непросто, но что делать… Основная проблема – поймать момент «выгрузки» карты. Я перебрал несколько вариантов и остановился на следующем.
1.Ставим на нашу переменную ТОЧКУ ОСТАНОВА.
2.Как только War попытается занять эту память под что-то другое, она сработает (возникнет исключение), и мы поймём, что нужно сканировать заново.
Что такое точки останова, программисту объяснять не нужно – любой отладчик (тот же Delphi) умеет их ставить. А теперь мы сами выступаем в роли отладчика и будем расставлять точки останова в War’е.
Главная проблема заключается в том, что точка останова действует только в контексте потока. Т.е. нам нужно будет ставить точки останова на каждый поток War. Вот теперь-то нам их список и пригодится. Кстати, главный поток теперь тоже отлавливается:

CREATE_PROCESS_DEBUG_EVENT:begin
    inc(CountOfThreads);
    WarThreads[CountOfThreads].handle:=de.CreateProcessInfo.hThread;
    WarThreads[CountOfThreads].id:=de.dwThreadId;
   end;//of CREATE_PROCESS_DEBUG_EVENT
Чтобы упростить процесс установки и удаления точек останова, я создал 2 процедуры. Первая устанавливает точку по указанному адресу, а вторая удаляет её:

//поставить точку останова на поток (на запись)
procedure SetBPW(hThread,Adr:cardinal);
Var cont:_CONTEXT;
Begin
 cont.ContextFlags:=CONTEXT_DEBUG_REGISTERS;
 cont.dr7:=3+$D0000;              //тип ловушки - на запись
 cont.dr0:=Adr;                   //адрес точки останова
 SetThreadContext(hThread,cont);  //ставим точку в контекст
End;

//Удаление точки останова
procedure DeleteBP(hThread:cardinal);
Var cont:_CONTEXT;
Begin
 cont.ContextFlags:=CONTEXT_DEBUG_REGISTERS;
 cont.dr7:=0;                     //сброс всех точек потока
 SetThreadContext(hThread,cont);  //сбрасываем...
End;
Далее вводится флаг AutoSet. Когда он равен true, это означает, что переменная уже найдена и далее точки останова ставятся на каждый вновь созданный поток. Итак, мы находим переменную, ставим на неё точки останова (во всех потоках) и указываем, что дальше они должны ставиться автоматом:

  //5. Устанавливаем точки останова (на запись)
  for ii:=1 to CountOfThreads do SetBPW(WarThreads[ii].handle,Adr);
  AutoSet:=true;                   //автоустановка точек
Далее до тех пор, пока одна из точек не сработает, просто отслеживаем мышь:

  //6. До перезапуска карты регулярно записываем позицию курсора мыши
  while AutoSet do begin
   ProcessEvents(40);
   GetCursorPos(pt);
   j:=(pt.y shl 16)+pt.x;
   WriteProcessMemory(pi.hProcess,pointer(Adr),@j,4,tmp);
  end;//of while
Несколько изменена и процедура ProcessEvents. Вы, надеюсь, ещё не забыли, что именно она обрабатывает все события? И сообщение о сработавшей точке останова получит она же. Оно будет оформлено в виде исключения и кодом EXCEPTION_SINGLE_STEP. И как только таковое случается, мы сразу же удаляем все до единой точки останова (т.к. они свою роль уже сыграли) и начинаем сканирование памяти сызнова (обратите внимание на бесконечный цикл repeat…until, которым обрамлён основной блок программы). Итак, вот как обрабатывается срабатывание точки останова:

    if de.Exception.ExceptionRecord.ExceptionCode=EXCEPTION_SINGLE_STEP
    then begin //сработала точка останова - грузится карта
     //1. Удалить все точки останова
     for i:=1 to CountOfThreads do DeleteBP(WarThreads__.handle);
     //2. Указать, что пора повторить цикл
     AutoSet:=false;
     ContinueDebugEvent(de.dwProcessId,de.dwThreadId,DBG_CONTINUE);
     exit;
    end;//of if (EXCEPTION_SINGLE_STEP)
Вот теперь практически всё. Готовый исходник находится в каталоге Ex6. Запустите его – и он будет стабильно отслеживать мышь, безо всяких вылетов. Даже после перезагрузки карты.
Если вы пишете что-то в несколько переменных, достаточно поставить точку останова лишь на одну из них – все остальные находятся в том же блоке памяти. Более того: так можно сильно ускорить поиск. Если в блоке найдена одна из переменных, то все остальные нужно искать там же, а не просматривать всю память с самого начала. Ну разве это не круто?

Просмотров: 7 492

Артас Менетил #1 - 6 лет назад 1
А можно ли отловить как-нибудь, в какую конкретно карту зашёл юзер?
ZeToX2007 #2 - 5 лет назад 1
в памяти содержится строка, с именем карты.
Player #3 - 4 года назад 1
А можно будет сделать так чтобы в карте юнит бежал к курсору ?
Харгард #4 - 4 года назад 1
Варден за это не банит?
Nikir #5 - 4 года назад 1
Блин, не совсем понятно как добавить поиск еще одной переменной, где что добавить?