#1 22-02-2008 19:59

listener
From: Vice City
Registered: 09-11-2006
Posts: 616
Website

HOWTO: Вызов функций gta_sa.exe и замена их собственным кодом.

Беда скриптовых языков состоит в том, что, вне зависимости от мощности языка, рано или поздно, наступает момент, когда возможностей скрипта становится недостаточно. Если возможности языка предусматривают расширяемость (например, через подключение внешних модулей), большой проблемы в этом нет. Но, если скриптовый двежок таких возможностей не предусматривает, приходится изыскивать обходные пути.

Сейчас, почти любой более-менее серьезный мод (и многие несерьезные) патчит память с той или иний целью. Нет большой проблемы в том, чтобы поправить один-два-три байта, но, если требуется вставить значительный кусок - сложность задачи очень быстро возрастает до недостижимого уровня. Гораздо проще, подключить собственный модуль (например, через ASI loader), но, здесь есть тонкость: подключаемый код должен быть согласован с имеющимся. Т.е., не портить стэк и внутренние структуры программы.

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

[large]1. Calling convention[/large]

С самим вызовом функции проблем нет: объявляем указатель на функцию, присваиваем ему адрес функции (со всеми необходимыми преобразованиями) и вызываем. Все остальное, компилятор сделает самостоятельно. Проблемы начинаются, если передаются параметры.

Способ передачи параметров, называется calling convention. Единого стандарта, по которому в функцию передаются параметры, не существует. Параметры могут передаваться на стэке или в регистрах, в прямом или обратном порядке; очистка стэка может производиться вызываемой или вызывающей функцией...

GTA написан на MSVC. В нем используется три типа calling convention: __cdecl, __stdcall и __thiscall.

__cdecl
__cdecl - традиционный метод передачи параметров, который идет еще со времен Кернигана и Ритчи. Параметры помещаются в стэк, начиная с последнего; чистит стэк вызывающая функция (это нужно для того, чтобы можно было передавать переменное число параметров).
Определить __cdecl-функцию в коде очень просто: она заканчивается retn без параметров.

Объявлять такие функции в своем коде, можно, например, так:

char * (__cdecl * gta_strtok)(char * str, char * delimiter) =  (char * (__cdecl *)(char *, char *))0x82244B;

После этого, можно вызывать gta_strtok, как обычную функцию.

__stdcall
__stdcall очень похож на __cdecl, за тем исключением, что стэк чистит вызываемая функция. Это немного проще и немного быстрее, но не позволяет передавать переменное число параметров. В конце __stdcall-функции, идет retn с параметром.

__thiscall
__thiscall, как можно догадаться, применяется, в основном, для вызова нестатических методов. (Но, никто не мешает, объявить как __thiscall и обычную функцию). Отличие метода от функции в том, что ему передается первый, неявный параметр this - указатель на объект, от которого вызывается метод. Этот неявный параметр, MSVC передает в регистре ECX. Т.е., если в функции используется, но не инициализируется ECX - это __thiscall, вне зависимости, чем она заканчивается. (Обычно, одной из первых команд, параметр из ECX сохраняется в локальной переменной или каком-нибудь менее часто используемом регистре, например MOV ESI, ECX)

Если мы описываем как __thiscall не метод объекта, а обычную функцию, this нужно описывать явно. Например, если нужно вызвать CModelCars::createInstance, не описывая полностью CModelCars со всей его иерархией, можно объявить метод так:

RwObject * (__thiscall * cars_createInstance)(CModelCars * _this, RwMatrix * modellingMatrix) = (RwObject * (__thiscall *)(CModelCars *, RwMatrix *))0x4C5110

и дальше, спокойно ее использовать, явно передавая указатель на объект модели.

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

DWORD (__thiscall * cars_createInstance)(DWORD _this, DWORD modellingMatrix) = (DWORD (__thiscall *)(DWORD, DWORD))0x4C5110

... продолжение следует ... (вопросы приветствуются).

Offline

#2 29-02-2008 16:57

Seemann
Registered: 07-08-2006
Posts: 2,155

Re: HOWTO: Вызов функций gta_sa.exe и замена их собственным кодом.

Вопрос первый: когда будет продолжение?

Offline

#3 03-03-2008 22:04

listener
From: Vice City
Registered: 09-11-2006
Posts: 616
Website

Re: HOWTO: Вызов функций gta_sa.exe и замена их собственным кодом.

Seemann
Хороший вопрос. Я надеюсь, что и ответ на него получился неплохим.

[large]2. Адресное пространство или немного кусочков теории[/large]

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

Для каждой программы выделяется отдельное адресное пространство в 4 гигабайта (для 32-х битного режима), из которого для самой программы, windows отводит два или три гигабайта, занимая остальную часть под системные модули. Это означает, что адрес, скажем 0x410000 в каждом запущеном процессе ссылается на ячейку памяти именно этого процесса, и добраться до нее из другого процесса просто так не получится.

В качестве штатного механизма, в winapi предусмотрена группа функций OpenProcess/ReadProcessMemory/WriteProcessMemory. Достоинством этого способа, можно назвать простоту и возможность использования даже из Visual Basic. К сожалению, адекватно построить блок кода, чтобы записать его в адресное пространство, можно только на ассемблере (от чего мы, изо всех сил, пытаемся уйти). Вдобавок, возникнет целая группа проблем, связанная с синхронизацией между процессами. Последним (но не по значимости), можно упомянуть то, что на момент, когда мы получили доступ к памяти процесса, процесс уже проинициализирован, и повлиять на ход инициализации невозможно.

К счастью, есть более эффективный, и более простой способ. Любая .dll, загруженная процессом, живет в адресном пространстве процесса. Кроме того, если она загружается вместе с процессом, функция DllMain вызывается до того, как процесс получит управление. (т.е., можно управлять инициализацией так, как нам нужно).

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

Правка import section необходима в тех случаях, если приложение использует только системные библиотеки, либо, если в библиотеках приложения слишком много подключаемых функций (да, можно конечно, написать хуки для ста функций, но это достаточно трудоемко, не говоря уже о том, что нудно). Этот способ полностью универсальный, но ощутимо трудоемкий.

К счастью, gta_sa.exe использует vorbis.dll, который хорошо документирован, и, из которого подключется всего девять функций. Все, что остается - написать собственную dll, в которой, на замену оригинальной dll и инициализацию потребуется меньше ста строк кода. После этого, все адресное пространство наше, и можно творить все, что вздумается.

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

Напоследок, стоит упомянуть, что защищенный режим называется так не зря: по умолчанию сегмент кода защищен от модификации, и попытка записать в него что-то, неминуемо приведет к GPF. Чтобы этого не происходило, требуется открыть модифицируемую область на запись при помощи функции VirtualProtect.

Пример (открывает на запись весь сегмент кода):

DWORD oldProtect;
VirtualProtect ((LPVOID)0x401000, 0x456000, PAGE_EXECUTE_READWRITE, &oldProtect);

На этом, теоретическая часть заканчивается, дальше будет только практика.

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

PPS. В третьей части будет самое интересное - внедрение функций (и еще немного о вызове функций из .exe). Вопросы, по-прежнему, приветствуются.

Offline

#4 04-03-2008 15:20

Seemann
Registered: 07-08-2006
Posts: 2,155

Re: HOWTO: Вызов функций gta_sa.exe и замена их собственным кодом.

Вопрос: насколько необходимо восстанавливать предыдущий режим ("execute", "read only") после того как область памяти открыта на запись? Обычно все примеры изменения памяти выглядят примерно так:

VirtualProtect открываем на запись
меняем код
VirtualProtect возвращаем предыдущий режим

Насколько безопасен твой пример с открытием на запись сразу всего кода?

Offline

#5 04-03-2008 15:36

Sanchez
Registered: 18-08-2006
Posts: 280

Re: HOWTO: Вызов функций gta_sa.exe и замена их собственным кодом.

А если на секцию кода установить (пропатчить) атрибут для записи, можно обойтись без VirtualProtect или его надо использовать в любом случае?

Offline

#6 04-03-2008 16:05

listener
From: Vice City
Registered: 09-11-2006
Posts: 616
Website

Re: HOWTO: Вызов функций gta_sa.exe и замена их собственным кодом.

Seemann
Практически полностью безопасен. Код закрывают на запись, в первую очередь, чтобы сразу было видно ошибки (когда рушится какой-нибудь указатель, и обращение идет куда попало). Как правило, распаковщики программ, защиты и прочие вещи, модифицирующие сегмент кода, открывают код на запись.

Sanchez
Да, именно так. При загрузке модуля, система выделяет блок памяти для каждой секции и делает сама делает VirtualProtect для каждой секции, согласно флагам, указанным в заголовке (вернее, если быть абсолютно точным, там не выделяется память, как таковая, а делается MapViewOfFile).

Offline

#7 24-03-2008 07:45

listener
From: Vice City
Registered: 09-11-2006
Posts: 616
Website

Re: HOWTO: Вызов функций gta_sa.exe и замена их собственным кодом.

Я жив, но не совсем здоров (погода простудная, блин), продолжение будет, как выздоровею.

Пока, проверяя примеры к третьей части, наткнулся на то, что свои функции объявить, как __thiscall, MSVC не дает - ругается, что это не метод объекта.
Ищу методы обхода.

Offline

Board footer

Powered by FluxBB