Сейчас 14:27:38 Воскресенье, 17 ноября, 2024 год
Главная ⇒ Форум ⇐ RSS Файлы Cтатьи Картинки В о й т и   или   з а р е г и с т р и р о в а т ь с я

Меню сайта

Категории

Наш опрос
Ваша любимая раса?
Проголосовало: 177949

Сейчас на сайте
На сайте всего: 79
Гостей: 79
Пользователей: 0

Реклама

Главная » Статьи по WarCraft 3 » Прочее » Разное
WarCraft: Взгляд изнутри Часть II: Затолкать дрянь
Этот таинственный ассемблер
Ну вот, к этому моменту все читатели уже разбежались (или уснули) :end:, так что самое время пускать в ход тяжёлую артиллерию. Держитесь крепче: эту программу мы напишем на чистом ассемблере! Причём сделаем её универсальной – достаточно поменять пару строк, и она будет запускать программу X, отслеживать обращения к файлу Y и перенаправлять их на файл Z.
Вначале немного об ассемблере. Не бойтесь – в нём нет ничего страшного. Современный ассемблер – это уже не тот монстр, который ещё 10 лет назад был кошмаром любого программиста. [Одна линковка чего стоила, с бесконечным подбором многочисленных ключей…] Теперь это – довольно простой язык, во всяком случае, не сложнее Си. Более того: ассемблер – самый доступный язык программирования. Достаточно закачать 780Кб (бесплатный пакет!) – и у вас в руках будет средство, способное создавать программы для DOS/Windows/Linux/BeOS/MeOS, obj-файлы, драйвера всех видов и под любые x86-процессоры (16/32/64-разрядные); всякие специфические штучки (бут-сектора, программы BIOS и пр.). Если же у кого-то проблемы с трафиком, можно закачать уменьшенный пакет (150Кб, без редактора – только компилятор и набор включаемых модулей).
Почему же asm’ом так редко пользуются? Первая и основная причина – практически полное отсутствие документации. Ну вот не привлекает он почему-то внимания авторов книг серии «для чайников». Если в комплекте со всеми остальными языками и средами разработки идёт здоровенный HELP (который подчас весит больше всего остального), то для asm’а ничего такого не предусмотрено. Согласитесь, что «методом тыка» изучить даже самый простой язык невозможно. В сети нормальной инфы тоже нет – сведения приходится собирать буквально по крупицам. Их основным источникам служат всё те же Microsoft SDK/DDK, AMD Architecture Programmer’s manual (вроде бы есть ещё аналогичный мануал от Intel, но я его не нашёл) и, конечно, ассемблерные исходники.
Ещё одна проблема – чуть повышенное время разработки (написание программы на asm’е отнимает примерно на 10-15% больше времени, чем на Си). Впрочем, эти недостатки окупаются исключительной миниатюрностью программ: простейшие «безоконные» программы занимают около 2Кб, а программы, имеющие собственное окошко – 4Кб. Разумеется, это минимальные цифры – чем больше функциональность, тем больше и вес. Но всё равно он будет значительно меньшим, чем у Delphi-приложений. Более того: даже эти миниатюрные программы легко сжимаются архиваторами (в zip-архиве - примерно вдвое).

Так что открываем каталог ExMod и смотрим…
А там есть целых 3 файла. Причём 2 из них стандартные:

* standart.inc – набор макросов, упрощающих программирование. Их я написал уже давно и теперь всё время пользуюсь (вроде как собственная библиотека).
* debug.inc – файл, содержащий описания констант и структур Debug API. Представляет собой конверсию сишных SDK/DDK-включаемых файлов под ASM.
* warmod.asm – собственно программа.

Для компиляции потребуется ассемблер fasm версии не ниже 1.65 (сейчас доступна 1.65.17 – новые версии выходят раз в 2-3 месяца).
Ну ладно. Прежде чем лезть в дебри ассемблерного кода, разберём собственно алгоритм – как можно заставить War заглотить «не тот» архив. Всё просто: прежде чем работать с файлом, его нужно открыть. War открывает файлы с помощью функции CreateFileA. Всё, что нам нужно сделать – отследить вызов этой функции, проверить её параметры, и если имя открываемого файла равно war3patch.mpq, заменить его на war3mod.mpq. И всё! Теперь о том, как отследить вызов нужной функции. Самый простой метод – сплайсинг, но он не годится для локального перехвата (т.е. сплайсингом мы перехватим вызов функции ВСЕМИ процессами Windows, что нам совершенно не нужно). Поэтому воспользуемся другим способом. Он тоже довольно прост: ставим точку останова на функцию, и когда она сработает – смотрим, что там с параметрами. Как уже говорилось ранее, точки останова должны ставиться на каждый поток индивидуально, поэтому ничего лишнего мы не затронем.
Тут, правда, есть одна проблема: в предыдущем примере точки останова удалялись нами сразу после их срабатывания. Здесь так поступить нельзя – точка должна висеть всё время работы War’а, т.к. он в любой момент может «связаться» с MPQ. Поэтому нам придётся заставлять War «проскакивать» точки останова (т.е. заставить его всё-таки выполнить функцию с изменёнными параметрами, несмотря на то, что точка по-прежнему стоит). Делается это так:

1. Убираем (временно) сработавшую точку останова;
2. Устанавливаем в контексте потока флаг TF;
3. После этого, когда то место, где стояла точка останова, будет пройдено, возникнет исключение EXCEPTION_SINGLE_STEP, и мы возвращаем точку останова на её законное место.

Вот. Ну что ж, приступим…
Ну, любая программа начинается с заголовка, и ассемблерная – не исключение. В заголовке указывается, под какую ОС мы будем компилировать программу, под какой процессор и что это вообще за программа (библиотека, драйвер, линкуемый файл, простой exe-файл и т.д.). Там же указывается точка входа (т.е. позиция, с которой начнётся выполнение программы).
Далее подключаются необходимые файлы (директивой include). Напоминает Си, не правда ли?
В самом конце программы я объявляю все необходимые переменные (хотя их можно объявить в любом месте программы). ASM может работать с виртуальными переменными (память под них выделяется динамически), чем я и пользуюсь:

virtual at ebp
C_1: ;для вычисления размеров
pi PROCESS_INFORMATION ;информация о процессе
stui STARTUPINFO ;стартовая информация
de DEBUG_EVENT ;отладочная информация
cont CONTEXT ;контекс потока
V_SIZE = $-C_1 ;размер блока виртуальных переменных
end virtual

Такой приём позволяет сэкономить на размере экзешника, хотя работать с виртуальными переменными чуть сложнее, чем с обычными.
Итак, в начале программы провожу всевозможную инициализацию. В частности, помещаю 0 в ebx (зачем – объясню позже), выделяю память под виртуальные переменные и очищаю её (забиваю нулями).

;1. Всякая инициализация
xor ebx,ebx ;помещаем 0 в ebx
sub esp,V_SIZE ;выделяем память
mov ebp,esp
xor eax,eax ;для очистки блока
mov edi,ebp
mov ecx,V_SIZE/4 ;кол-во двойных слов
rep stosd ;очистка

Теперь нужно выделить память под динамический массив, где будут храниться хэндлы потоков (вам это знакомо по предыдущим примерам):

invoke HeapAlloc,,HEAP_ZERO_MEMORY,4096
mov [hWarThreads],eax ;выделить память под массив

Следующее наше действие – получить адрес функции CreateFileA, чтобы после запуска War’а спокойно поставить туда точку останова. Найденный адрес сохраняется в переменной dwEntry:

;2. Получим адрес точки входа функции CreateFileA
invoke GetProcAddress,,szCrFile
mov [dwEntry],eax ;сохранить полученное

Всё, что начинается с префикса “sz” – строки (имена). Я их все вынес в таблицу строк – в конце файла. Можно было бы подставлять и непосредственно строки, но использование таблицы строк уменьшает размер экзешника и увеличивает скорость его работы (ещё один плюс – все строки собраны в кучу, в случае чего их не надо разыскивать по всему коду).
Теперь – пускаем War. Это осуществляется всё той же функцией CreateProcess:

;3. Пускаем War
mov [stui.cb],sizeof.STARTUPINFO ;размер структуры
mov [stui.dwFlags],STARTF_USESHOWWINDOW
mov [stui.wShowWindow],SW_SHOWNORMAL
lea eax,[pi]
push eax
lea eax,[stui]
push eax
invoke CreateProcess,szName,ebx,ebx,ebx,\
ebx,DEBUG_PROCESS or DEBUG_ONLY_THIS_PROCESS,\
ebx,ebx

Как видим, виртуальные переменные нельзя передавать непосредственно в качестве параметров функции, это делается посредством команд lea/push. Как вы помните, в ebx ещё с начала программы лежит 0. А это число очень часто используется в качестве параметров функции. Передача непосредственно нуля требует 2 байт кода, а передача содержимого регистра (в котором лежит тот же ноль) – только одного байта. Т.е. на вызове CreateProcess мы сэкономили 6 байт! Я часто пользуюсь таким трюком, благо это несложно.
Итак, War запущен. Теперь начинаем обрабатывать события:

lea eax,[de]
invoke WaitForDebugEvent,eax,INFINITE ;ждать события отладки
test eax,eax
jz l_exit ;ошибка

Как вы помните, de – тоже виртуальная переменная, передаётся через eax (по lea). Как известно, в случае какой-либо ошибки WaitForDebugEvent возвращает 0, чем мы и пользуемся (выходим из цикла. test/jz – укороченная разновидность IF, занимает на 3 байта меньше).
Далее идёт анализ событий. Прежде всего, отслеживается создание потоков. Хэндлы всех созданных потоков собираются в массиве hWarThreads:

.if [de.dwDebugEventCode]=CREATE_PROCESS_DEBUG_EVENT
mov edx,[de.CreateProcessInfo.hThread] ;хэндл потока (параметр)
call AddThread ;добавить поток в список
.endif ;of CREATE_PROCESS_DEBUG_EVENT

.if [de.dwDebugEventCode]=CREATE_THREAD_DEBUG_EVENT
mov edx,[de.CreateThread.hThread] ;хэндл потока
call AddThread ;добавить поток в список
.endif ;of CREATE_THREAD_DEBUG_EVENT

Здесь AddThread – процедура, добавляющая хэндл потока в массив и устанавливающая в контексте этого потока точку останова. На входе в процедуру в edx должен лежать хэндл потока для добавления:

AddThread: ;процедура
inc [dwCountOfThreads] ;увеличить кол-во потоков
mov eax,[dwCountOfThreads]
mov ecx,[de.dwThreadId] ;сохраняем ID потока
push edi
mov edi,[hWarThreads]
mov [edi+eax*8],ecx
mov [edi+eax*8+4],edx ;сохраняем хэндл потока
pop edi
SetBP: ;разделяемая процедура (edx - хэндл)
mov [cont.ContextFlags],CONTEXT_DEBUG_REGISTERS
mov [cont.iDr7],3 ;точка останова на выполнение
mov eax,[dwEntry]
mov [cont.iDr0],eax ;адрес точки останова
lea eax,[cont]
invoke SetThreadContext,edx,eax ;установить
ret

Интересная особенность ассемблера – «короткость» многих инструкций. Поэтому ассемблерные программы выглядят довольно оригинально – «столбиком». Если поток завершился, его хэндл нужно удалить из списка:

.if [de.dwDebugEventCode]=EXIT_THREAD_DEBUG_EVENT
;удаляем поток из списка:
call FindThreadHandle
;поток найден. Удаляем из массива...
mov edx,[dwCountOfThreads] ;номер последнего элемента
mov ecx,[hWarThreads] ;адрес массива
push dword [ecx+edx*8]
pop dword [eax]
push dword [ecx+edx*8+4]
pop dword [eax+4]
dec [dwCountOfThreads] ;уменьшить кол-во потоков
.endif ;of EXIT_THREAD_DEBUG_EVENT

Манипуляции push/pop обеспечивают удаление хэндла, затем счётчик потоков уменьшается на 1. Как вы уже догадались, FindThreadHandle – функция, которая ищет хэндл в списке (для последующего удаления). Она находится в начале кода:

FindThreadHandle:
mov eax,[hWarThreads] ;адрес массива
mov edx,[de.dwThreadId] ;ID потока для сравнения
;ищем нужный поток (по хэндлу)
.for ecx=[dwCountOfThreads],ecx>0,ecx--,eax+=8
.exitf edx=[eax] ;выход: нашли поток!
.endf
ret

Цикл for – наиболее оригинальный фрагмент всей функции (хотя те, кто программирует на Си, к такому уже привыкли). На Delphi этот цикл можно перевести как

for ecx:=dwCountOfThreads downto 0 do begin
if edx=eax^ then exit;
eax:=eax+8;
end;

Согласитесь, что asm-версия гораздо нагляднее (Си пользуется аналогичной конструкцией).
Теперь анализируем событие типа EXCEPTION_DEBUG_EVENT. Прежде всего, если это исключение типа EXCEPTION_BREAKPOINT, то продолжить выполнение процесса (используя флаг DBG_CONTINUE):

;проверим, какое возникло исключение:
.if [de.Exception.pExceptionRecord.ExceptionCode]=EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent,[de.dwProcessId],[de.dwThreadId],DBG_CONTINUE
jmp l_loop ;continue
.endif ;of EXCEPTION_BREAKPOINT

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

;получим хэндл потока, вызвавшего исключение
call FindThreadHandle ;найти хэндл потока
mov esi,[eax+4] ;считываем найденный хэндл

Затем проверим, что послужило причиной исключения. Для этого мы читаем содержимое контекста потока. И если там нет точки останова – значит, исключение пришло от флага TF (прохождение того места, где эта точка была). И точку надо установить заново:

;определим причину исключения
mov [cont.ContextFlags],CONTEXT_DEBUG_REGISTERS
lea eax,[cont]
invoke GetThreadContext,esi,eax ;получаем содержимое регистров
.if [cont.iDr0]=0
mov edx,esi ;хэндл потока
call SetBP ;установить точку останова (заново)
jmp l_continue ;продолжить
.endif ;of Dr0=0

В противном случае исключение пришло от точки останова. А значит, War пытается открыть некий файл. Прежде всего, снимем эту точку останова и установим флаг TF (трассировочный, для последующей установки точки):

;установим флаг трассировки и снимем точку останова
mov [cont.iDr0],ebx ;0
lea eax,[cont]
invoke SetThreadContext,esi,eax ;установить

mov [cont.ContextFlags],CONTEXT_CONTROL
lea eax,[cont]
invoke GetThreadContext,esi,eax
or [cont.regFlag],FLAG_TF ;TF

Теперь начинаем анализировать параметры функции. Вначале читаем указатель на имя файла, а затем и само имя:

;проверим параметры функции
mov eax,[cont.regEsp] ;считать содержимое esp
add eax,4 ;получить указатель на имя файла
invoke ReadProcessMemory,[pi.hProcess],eax,dwAddr,4,ebx
invoke ReadProcessMemory,[pi.hProcess],[dwAddr],uBuf,14,ebx

А теперь проверим, не war3patch.mpq ли это. Для сравнения строк используем lstrcmpi, которая сравнивает строки без учёта регистра символов (Си тоже так умеет). И если это – war3patch.mpq, записываем туда другое имя:

invoke lstrcmpi,uBuf,szFile ;сравним строки
.ifz eax ;это - war3patch.mpq
invoke WriteProcessMemory,[pi.hProcess],[dwAddr],szNewFile,N_SIZE,ebx
.endif

Затем просто доустанавливаем контекст.
Вот, в принципе, и всё – далее идут лишь всякие «обёртки» цикла и завершение программы. Как видите, всё не так уж сложно.
Компилируем программу… И видим, что её размер равен 2Кб! (Кстати, размер программы всегда округляется asm’ом до величины, кратной 512 байтам).

Просмотров: 1777 Добавил: РеКсАр Добавлено: 09 Июля 2007 в 10:16:57
Комментариев: 1 |

Всего комментариев: 1
25 Августа 2012
1. Букреев Николай (SirNikolas) [Материал]

Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]

Форма входа

Поиск

Случайная картинка

Случайный файл
[15 Июня 2008]
[Карты · Кампании]
Ilidan's Revenge -
  • Автор: Elva
  • Жанр: Кампания
  • Количество глав: 1
  • Сложность: Normal
  • Язык: Английский

    Ослабленный Иллидан после финальной схватки с Артасом у Ледяной Короны должен найти способ, как восстановить свои силы.
    Кампания состоит из одной главы, в начале вступительный ролик.


  • Новые карты
    [07 Февраля 2016]
    Переезжаем на другой сайт, господа![Dota]
    [18 Октября 2015]
    Duel of Gods PreV[Другое]
    [18 Октября 2015]
    Hero of The Empire v1.18g[RPG]
    [17 Октября 2015]
    Servant War v1.05[Другое]
    [17 Октября 2015]
    Age of Vikings Edited v1.6[Другое]
    [17 Октября 2015]
    Strife of the Champions Beta v1.2[Arena]
    [17 Октября 2015]
    VirusBoll (rus)[Другое]
    [17 Октября 2015]
    Exterminators v1[AoS]
    [17 Октября 2015]
    The Lord Heroes v1.2[Другое]
    [17 Октября 2015]
    Versus heroe Arena 1.0 AI[Arena]

    5 лучших по кол-ву добавленных статей
    [ Duosora ] [ 58 ]
    [ Messenger ] [ 52 ]
    [ Bru ] [ 39 ]
    [ Pand@ ] [ 35 ]
    [ OrcRider ] [ 27 ]

    Наша кнопка
    Warcraft3FT.info - Всё для Warcraft 3 и DotA

    Другие варианты

    Статистика

    Материалы:
    Новости: 1010
    Файлы: 8668
    Статьи: 680
    Картинки: 8256
    Форум: 30520/954989
    Комментарии: 58094
    Copyright © 2006 - 2024 Warcraft3FT.info При копировании материалов c сайта ставьте, пожалуйста, активную обратную ссылку на нас • Design by gReeB04ki ©
    Хостинг от uCoz