20121031



На картинке - наглядная демонстрация, как выглядит Virtual Method Table на более низком уровне. Первая строчка - закомментированный вызов той же самой функции, но при помощи макроса, следующие три - уже без макроса.

Помещаем содержимое pd3d в eax, помещаем содержимое этого адреса опять в eax, обращаемся к нужной функции используя полученный адрес. Помните, что я в видео говорил про двойной указатель? Вот, как бы, он и есть.

Если по-человечески, то выглядеть это будет как-то вот так:

pd3d->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm);

А вот чтобы выковырять адрес функции из pd3d, нам (или сишному компилятору) нужно сделать такой вот финт ушами, как на картинке выше.

PS: Если вы вдруг читаете заголовочные файлы D3D перед сном, то наверняка заметили, что в 99% функций первым аргументом идёт или D3DObject или D3DDevice. Смекаете, а? Можно перехватить любую функцию и получить доступ к актуальному на данный момент объекту\устройству.
Я тут начитался всяких крутых форумов и вот, что получилось в процессе экспериментирования:


Это, товарищи, обыкновенный Prince of Persia: Sands of Time, только вот ему было сказано не заполнять полигоны текстурами, а оставлять только сетку. Wireframe mode, типа.

PS: Я не ожидал, что wallhack и прочие подобные читы делаются настолько просто, пусть и в довольно сыром виде. Вот и будет тема следующих уроков, когда загрузчик для меню допишем. :)

20121030

Если вдруг кому интересно, почему в GetDevice9Methods() идёт вот такое смещение:

present9 = vtablePtr[17] - (DWORD)hD3D9;

То вот картинка:


Там 18*4 (4 - размер DWORD, потому что это таблица указателей), отнимаем один адрес, т.к. это адрес самой таблицы. Так-то.

D3D: Новый алгоритм и работа со шрифтами.

Да-да, видеоурок с подробными объяснениями, но достаточно плохимм звуком. Выложу тут ссылку (видео будет доступно как на youtube, так и на форуме gamehacklab.ru), как только оно обработается.

[Ссылка] на урок.

Собственно, полный исходный код из видео:


#include
#include
#pragma comment(lib,"d3dx9.lib")

typedef IDirect3D9* (__stdcall *DIRECT3DCREATE9)(unsigned int);
typedef long (__stdcall *PRESENT9)(IDirect3DDevice9* self, const RECT*, const RECT*, HWND, void*);

PRESENT9 g_D3D9_Present = 0;
BYTE g_codeFragment_p9[5] = {0, 0, 0, 0, 0};
BYTE g_jmp_p9[5] = {0, 0, 0, 0, 0};
DWORD g_savedProtection_p9 = 0;
DWORD present9 = 0;
bool indicator = 0;
D3DRECT rec = {10, 10, 120, 30};
ID3DXFont *m_font = 0;
RECT fontRect = {10, 15, 120, 120};
D3DCOLOR bkgColor = 0;
D3DCOLOR fontColor = 0;

void DrawIndicator(void* self)
{
        IDirect3DDevice9* dev = (IDirect3DDevice9*)self;
        dev->BeginScene();
D3DXCreateFont(dev, 12, 0, FW_BOLD, 0, 0, 1, 0, 0, 0 | FF_DONTCARE, TEXT("Arial"), &m_font);
if(indicator)
{
bkgColor = D3DCOLOR_XRGB(0, 0, 255);
fontColor = D3DCOLOR_XRGB(0, 255, 255);
}
else
{
bkgColor = D3DCOLOR_XRGB(255, 0, 0);
fontColor = D3DCOLOR_XRGB(255, 0, 0);
}
        dev->Clear(1, &rec, D3DCLEAR_TARGET, bkgColor, 1.0f, 0);
m_font->DrawText(0, "keng.gamehacklab.ru", -1, &fontRect, 0, fontColor);
        dev->EndScene();
}

void GetDevice9Methods()
{
HWND hWnd = CreateWindowA("STATIC","dummy", 0, 0, 0, 0, 0, 0, 0, 0, 0);
HMODULE hD3D9 = LoadLibrary("d3d9");
DIRECT3DCREATE9 Direct3DCreate9 = (DIRECT3DCREATE9)GetProcAddress(hD3D9, "Direct3DCreate9");
IDirect3D9* d3d = Direct3DCreate9(D3D_SDK_VERSION);
    D3DDISPLAYMODE d3ddm;
    d3d->GetAdapterDisplayMode(0, &d3ddm);
    D3DPRESENT_PARAMETERS d3dpp;
    ZeroMemory(&d3dpp, sizeof(d3dpp));
    d3dpp.Windowed = 1;
    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
    d3dpp.BackBufferFormat = d3ddm.Format;
IDirect3DDevice9* d3dDevice = 0;
    d3d->CreateDevice(0, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &d3dDevice);
DWORD* vtablePtr = (DWORD*)(*((DWORD*)d3dDevice));
present9 = vtablePtr[17] - (DWORD)hD3D9;
d3dDevice->Release();
d3d->Release();
FreeLibrary(hD3D9);
CloseHandle(hWnd);
}

long __stdcall HookedPresent9(IDirect3DDevice9* self, const RECT* src, const RECT* dest, HWND hWnd, void* unused)
{
BYTE* codeDest = (BYTE*)g_D3D9_Present;
codeDest[0] = g_codeFragment_p9[0];
*((DWORD*)(codeDest + 1)) = *((DWORD*)(g_codeFragment_p9 + 1));
DrawIndicator(self);
DWORD res = g_D3D9_Present(self, src, dest, hWnd, unused);
codeDest[0] = g_jmp_p9[0];
*((DWORD*)(codeDest + 1)) = *((DWORD*)(g_jmp_p9 + 1));
return res;
}

void HookDevice9Methods()
{
HMODULE hD3D9 = GetModuleHandle("d3d9.dll");
g_D3D9_Present = (PRESENT9)((DWORD)hD3D9 + present9);
g_jmp_p9[0] = 0xE9;
DWORD addr = (DWORD)HookedPresent9 - (DWORD)g_D3D9_Present - 5;
memcpy(g_jmp_p9 + 1, &addr, sizeof(DWORD));
memcpy(g_codeFragment_p9, g_D3D9_Present, 5);
VirtualProtect(g_D3D9_Present, 8, PAGE_EXECUTE_READWRITE, &g_savedProtection_p9);
memcpy(g_D3D9_Present, g_jmp_p9, 5);
}

DWORD __stdcall TF(void* lpParam)
{
GetDevice9Methods();
HookDevice9Methods();
return 0;
}

DWORD __stdcall KeyboardHook(void* lpParam)
{
        while(1)
        {
                if(GetAsyncKeyState(VK_F1))  
                {
                        indicator = !indicator;
                        Beep(500,200);
                }
                Sleep(100);
        }    
        return 0;
}

int __stdcall DllMain(HINSTANCE hInst, DWORD  ul_reason_for_call, void* lpReserved)
{
        switch (ul_reason_for_call)  
        {
        case DLL_PROCESS_ATTACH:
CreateThread(0, 0, &TF, 0, 0, 0);
CreateThread(0, 0, &KeyboardHook, 0, 0, 0);
        }
        return 1;
}

Изменения с предыдущей версии:
-Новый алгоритм перехвата, похож на MS Detours - работает при помощи инъекции кода.
-Поддержка (какая-никакая) шрифтов.

В следующем видео будем свой инжектор для этого дела писать. :)

20121029

D3D: Новости

Засел я тут за нашу менюшечку, переписал алгоритм, так что он теперь немного другой и я всё-таки запишу по нему видеоурок, в котором всё постараюсь разжевать. Почему? Потому что теперь это ни что иное, как инъекция кода. :)

Сейчас курочу шрифты и загрузчик, ждите.

Маленькая поправка к коду D3D-хука

Я тут нашёл свои старые записи и выяснилось, что вот это место:


while(1)
        {
                Sleep(100);
                HookVTableFunction((PDWORD*)npDevice, (PBYTE)hkReset, 16);
                HookVTableFunction((PDWORD*)npDevice, (PBYTE)hkPresent, 17);
        }  

В функции, которую крутит поток TF, выполняет очень тупой костыль - каждые 100 миллисекунд переустанавливает хук, потому что как сама винда, так и игра очень этого дела не любят и во многих играх у меня в изначальной версии (без цикла) хук довольно быстро умирал сам по себе. С чем это поведение связано - не знаю, но разберусь.

Я это к тому, что если вдруг вы прочитали код и полностью в него вникли, а не просто ктрлц+ктрлв. :D

Пока что работаю над шрифтами, они и будут темой следующего видео.

20121028

Продолжаем ломать Prince of Persia: Sands of Time

Вроде как, более-менее починил звук на ноутбуке, так что попробую записать следующий урок. На повестке дня - One Hit Kill в PoP:SoT. Да, для самых постоянных читателей задание простенькое, но ведь это взлом по заявкам.

Назрела пара мыслей - подумываю перезаписать рубрику "Делаем трейнер с нуля", подумываю даже на чистом ассемблере, ибо в прошлый раз как-то это дело не пошло - вяло отзывались.

Ещё могу на C++, а могу что-нибудь доделать в C#. Отзывы, пожелания, предложения - в комментарии к этой записи.

Параллельно буду углубляться дальше в D3D, план примерно такой:

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

Как вы уже правильно догадались, вся кухня будет происходить именно в DLL, GUI будет выступать только в качестве загрузчика DLL в память игры. Соотсветственно, все функции для взлома будут лежать именно в DLL.

Почему всё это на C++? Потому что D3D - это интерфейс, а в ассемблере и просто C надо будет попотеть, чтобы сделать это так же просто, как в C++. Почему не C#? Потому что C# слишком абстрактен для достаточно низкоуровневых вещей и работе с указателями, но и там тоже можно всё это сделать.

Direct3D - Часть 1: Исходный код и его разбор

Ребята, всем привет!

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

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

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

Итак, что нам понадобится:

1. [Microsoft Visual Studio]. Express, ибо бесплатная, 2010 или 2012 - без разницы. Я использую 2012. Если используете 2012 - качайте версию для desktop-разработки, ага.
2. [DirectX SDK], на момент написания статьи - версия за июль 2010-го. Собственно, набор разработки (Lego) для тех, кто хочет что-нибудь рисовать под виндой.

!!!ВНИМАНИЕ!!!

Сначала ставим студию, потом - SDK!

!!!ВНИМАНИЕ!!!

Открываем студию, получаем вот такое окошко:


Видим манящую кнопку "New Project...", или же прожимаем Ctrl+Shift+N. В появившемся окне выбираем пустой проект на плюсах, внизу вбиваем ему какое-нибудь имя:


Больше ничего не трогаем и прожимаем Next, пока нам не покажут пустое окно студии. Всё верно, кода в нём нет. Сбоку находим "Solution Explorer", жмём на нём мышью, затем - правой кнопкой на "Source Files" и выбираем пункт "Add - New Item...":


Внизу обзываем его "main.cpp", жмём "Add", откроется пустое окно кода. Копируем туда вот это:


#include

typedef IDirect3D9* (STDMETHODCALLTYPE *DIRECT3DCREATE9)(UINT);
typedef HRESULT(WINAPI* tPresent)(LPDIRECT3DDEVICE9 pDevice, CONST RECT*, CONST RECT*, HWND, LPVOID);
typedef HRESULT(WINAPI* tReset)(LPDIRECT3DDEVICE9 pDevice, D3DPRESENT_PARAMETERS* pPresentationParameters);
typedef DWORD   (STDMETHODCALLTYPE *GETPROCESSHEAPS)(DWORD, PHANDLE);

static DWORD vtableFrag9[] = { 0, 0, 0, 0 };
static DWORD* presentPtr = 0;
static DWORD* resetPtr = 0;
static DWORD offsetPresent = 0;
static DWORD offsetReset = 0;
static tPresent g_D3D9_Present = 0;
static tReset g_D3D9_Reset = 0;
LPDIRECT3DDEVICE9 npDevice;

bool indicator = 0;

void DrawIndicator(LPVOID self)
{
        IDirect3DDevice9* dev = (IDirect3DDevice9*)self;
        dev->BeginScene();
        D3DRECT rec = { 10, 10, 30, 30 };
        D3DCOLOR color = 0;
        if(indicator)
        {
                color = D3DCOLOR_XRGB(0, 255, 0);
        }
        else
        {
                color = D3DCOLOR_XRGB(255, 0, 0);
        }
        dev->Clear(1, &rec, D3DCLEAR_TARGET, color, 1.0f, 0);
        dev->EndScene();
}

HRESULT WINAPI hkPresent(LPDIRECT3DDEVICE9 pDevice, CONST RECT* src, CONST RECT* dest, HWND hWnd, LPVOID unused)
{    
        while(!npDevice)
        {
        npDevice = pDevice;
        }
        DrawIndicator(pDevice);
        return g_D3D9_Present(pDevice, src, dest, hWnd, unused);
}

HRESULT WINAPI hkReset(LPDIRECT3DDEVICE9 pDevice, D3DPRESENT_PARAMETERS* pPresentationParameters)
{
        return g_D3D9_Reset(pDevice, pPresentationParameters);
}

BOOL SearchHeap(HANDLE heap)
{
        int vtableLenBytes = sizeof(DWORD)*4;
        PROCESS_HEAP_ENTRY mem;
        mem.lpData = 0;
        while(HeapWalk(heap, &mem))
        {
                if(mem.wFlags == PROCESS_HEAP_UNCOMMITTED_RANGE) continue;
                DWORD* p = (DWORD*)mem.lpData;
                for(int i = 0; i < (int)(mem.cbData/sizeof(DWORD)); i++)
                {
                        if(memcmp(p, vtableFrag9, vtableLenBytes) == 0)
                        {
                                presentPtr = p + 11;
                                resetPtr = p + 10;
                                offsetPresent = *presentPtr;
                                offsetReset = *resetPtr;
                                g_D3D9_Present = (tPresent)((DWORD*)offsetPresent);
                                g_D3D9_Reset = (tReset)((DWORD*)offsetReset);
                                break;
                        }
                        p++;
                }
                if(presentPtr != 0) break;
        }
        return(presentPtr != 0);
}

void CheckAndHookPresent9()
{
        if(presentPtr != 0 && (*presentPtr) == (DWORD)hkPresent) return;
        HANDLE heap = 0;
        HMODULE hKern = GetModuleHandleA("kernel32.dll");    
        GETPROCESSHEAPS getProcessHeaps = (GETPROCESSHEAPS)GetProcAddress(hKern, "GetProcessHeaps");
        if(getProcessHeaps != 0)
        {
                HANDLE heaps[1000];
                int numHeaps = (getProcessHeaps)(1000, heaps);
                for(int k = 0; k < numHeaps; k++)
                {
                        heap = heaps[k];
                        if(SearchHeap(heap)) break;
                }
        }
        else
        {
                heap = GetProcessHeap();
                SearchHeap(heap);
        }
        HeapLock(heap);
        if(presentPtr != 0)
        {
                (*presentPtr) = (DWORD)hkPresent;            
        }
        if(resetPtr != 0)
        {
                (*resetPtr) = (DWORD)hkReset;
        }
        HeapUnlock(heap);
}

void CopyVMT9(DWORD* vtableFrag)
{    
        HWND hWnd = CreateWindowA("STATIC","dummy", 0, 0, 0, 0, 0, 0, 0, 0, 0);
        HMODULE hD3D9 = GetModuleHandleA("d3d9");
        DIRECT3DCREATE9 Direct3DCreate9 = (DIRECT3DCREATE9)GetProcAddress(hD3D9, "Direct3DCreate9");
        IDirect3D9* d3d = Direct3DCreate9(D3D_SDK_VERSION);
        D3DDISPLAYMODE d3ddm;
        d3d->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm);
        D3DPRESENT_PARAMETERS d3dpp;
    ZeroMemory(&d3dpp, sizeof(d3dpp));
    d3dpp.Windowed = 1;
    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
        IDirect3DDevice9* d3dDevice = 0;
        d3d->CreateDevice(0, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &d3dDevice);  
        DWORD* vtablePtr = (DWORD*)(*((DWORD*)d3dDevice));
        for(int i = 0; i < 4; i++)
        {
                vtableFrag[i] = vtablePtr[i + 6];
        }
        d3dDevice->Release();
        d3d->Release();
        DestroyWindow(hWnd);
}

PBYTE HookVTableFunction(PDWORD* dwVTable, PBYTE dwHook, INT Index)
{
    DWORD dwOld = 0;
    VirtualProtect((void*)((*dwVTable) + (Index*4)), 4, PAGE_EXECUTE_READWRITE, &dwOld);
    PBYTE pOrig = ((PBYTE)(*dwVTable)[Index]);
    (*dwVTable)[Index] = (DWORD)dwHook;
    VirtualProtect((void*)((*dwVTable) + (Index*4)), 4, dwOld, &dwOld);
    return pOrig;
}

bool hooked = 0;
DWORD WINAPI TF(LPVOID lpParam)
{
        CopyVMT9(vtableFrag9);
        CheckAndHookPresent9();
        while(!npDevice && !hooked)
        {
                Sleep(50);
        }
        hooked = !hooked;
        while(1)
        {
                Sleep(100);
                HookVTableFunction((PDWORD*)npDevice, (PBYTE)hkReset, 16);
                HookVTableFunction((PDWORD*)npDevice, (PBYTE)hkPresent, 17);
        }  
        return 0;
}

DWORD WINAPI KeyboardHook(LPVOID lpParam)
{
        while(1)
        {
                if(GetAsyncKeyState(VK_F1))  
                {
                        indicator = !indicator;
                        Beep(500,200);
                }
                Sleep(100);
        }    
        return 0;
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
        switch (ul_reason_for_call)  
        {
        case DLL_PROCESS_ATTACH:
                {
                        CreateThread(0, 0, &TF, 0, 0, 0);
                        CreateThread(0, 0, &KeyboardHook, 0, 0, 0);
                }
        case DLL_PROCESS_DETACH:
                break;
        }
        return 1;
}

О, совсем забыл. Нам надо указать, что на выходе мы хотим не exe-файл, а dll. Меню "Project - Properties", вкладка "General", справа ищем "Configuration type: Application (.exe)", радостно меняем его на "Dynamic Library (.dll)", жмём OK.

Попробуем выяснить, собирается ли оно. Меню "Build - Build Solution". После этого внизу должно появиться что-то такое:


Значит, проект собрался и всё отлично. Если студия ругается, что не может найти заголовочный файл d3d9.h, то идём в свойства проекта, находим там вкладку "VC++ Directories", добавляем в "Include Directories" и "Library Directories" соответствующие папки из DirectX SDK, который будет лежать где-то в Program Files на диске цэ.

Итак, вроде всё хорошо, вернёмся к коду. Смотрим на самый главный метод - точку входа:


BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
        switch (ul_reason_for_call)  
        {
        case DLL_PROCESS_ATTACH:
                {
                        CreateThread(0, 0, &TF, 0, 0, 0);
                        CreateThread(0, 0, &KeyboardHook, 0, 0, 0);
                }
        case DLL_PROCESS_DETACH:
                break;
        }
        return 1;
}

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

В кратце, чем отличается dll от exe? Exe умеет запускаться сам, а вот dll должна быть пропихнута к уже запущенному процессу. Проверкой этого, собственно, метод и занимается - если ul_reason_for_call == DLL_PROCESS_ATTACH (присоединились к процессу), то запускаем два потока, а если DETACH (отсоединились от процесса), то ничего не делаем. После выполнения всегда возвращаем 1, то есть правду.

Если не в курсе про switch-case - почитайте, прикольная штука, я сейчас останавливаться подробно не буду.

Дык вот. Присоединились к процессу, запустили два потока. Что они, собственно, делают? Рассмотрим сначала функцию для потока KeyboardHook, потому что она покороче и попроще:


DWORD WINAPI KeyboardHook(LPVOID lpParam)
{
        while(1)
        {
                if(GetAsyncKeyState(VK_F1))  
                {
                        indicator = !indicator;
                        Beep(500,200);
                }
                Sleep(100);
        }    
        return 0;
}

Всё просто до безобразия. Крутимся в бесконечном цикле, в котором:

1. Если нажата F1, то
2. Меняем значение переменной indicator на противоположное (0\1)
3. Пиликаем динамиком (Beep)
4. Ждём 100 мсек

Если вдруг цикл прерывается - возвращаем 0.

...Ничего не напоминает? Правильно! Эта штука включает и выключает нашу менюшку.

Что же у нас крутится во втором потоке? Давайте посмотрим:


DWORD WINAPI TF(LPVOID lpParam)
{
        CopyVMT9(vtableFrag9);
        CheckAndHookPresent9();
        while(!npDevice && !hooked)
        {
                Sleep(50);
        }
        hooked = !hooked;
        while(1)
        {
                Sleep(100);
                HookVTableFunction((PDWORD*)npDevice, (PBYTE)hkReset, 16);
                HookVTableFunction((PDWORD*)npDevice, (PBYTE)hkPresent, 17);
        }  
        return 0;
}

Ого, а вот тут уже интересно! Метод CopyVMT9, судя по названию, что-то делает с VMT для 9-й версии библиотеки Direct3D, а именно - вот код:


void CopyVMT9(DWORD* vtableFrag)
{    
        HWND hWnd = CreateWindowA("STATIC","dummy", 0, 0, 0, 0, 0, 0, 0, 0, 0);
        HMODULE hD3D9 = GetModuleHandleA("d3d9");
        DIRECT3DCREATE9 Direct3DCreate9 = (DIRECT3DCREATE9)GetProcAddress(hD3D9, "Direct3DCreate9");
        IDirect3D9* d3d = Direct3DCreate9(D3D_SDK_VERSION);
        D3DDISPLAYMODE d3ddm;
        d3d->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm);
        D3DPRESENT_PARAMETERS d3dpp;
    ZeroMemory(&d3dpp, sizeof(d3dpp));
    d3dpp.Windowed = 1;
    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
        IDirect3DDevice9* d3dDevice = 0;
        d3d->CreateDevice(0, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &d3dDevice);  
        DWORD* vtablePtr = (DWORD*)(*((DWORD*)d3dDevice));
        for(int i = 0; i < 4; i++)
        {
                vtableFrag[i] = vtablePtr[i + 6];
        }
        d3dDevice->Release();
        d3d->Release();
        DestroyWindow(hWnd);
}

Если попроще, то он создаёт пустое и невидимое окно, получает его хэндл, создаёт для него новый D3D-объект и копирует кусочек VMT этого объекта. Затем всё уничтожает, но сохраняет кусочек VMT.

Фишка тут в том, что таблицы методов (VMT) получаются для разных D3D-объектов, ибо одновременно можно использовать только один (а его создаёт и использует игра), но вот по кусочку таблицы можно найти уже созданную ранее таблицу, которая и используется игрой. По механизму действия это похоже на поиск по сигнатуре.

В общем, после выполнения этого метода у нас есть "зацепка" - сигнатура VMT для D3D-объекта, который используется игрой в данный момент. Идём дальше. Что там дальше? Ага, CheckAndHookPresent9. Вот он:


void CheckAndHookPresent9()
{
        if(presentPtr != 0 && (*presentPtr) == (DWORD)hkPresent) return;
        HANDLE heap = 0;
        HMODULE hKern = GetModuleHandleA("kernel32.dll");    
        GETPROCESSHEAPS getProcessHeaps = (GETPROCESSHEAPS)GetProcAddress(hKern, "GetProcessHeaps");
        if(getProcessHeaps != 0)
        {
                HANDLE heaps[1000];
                int numHeaps = (getProcessHeaps)(1000, heaps);
                for(int k = 0; k < numHeaps; k++)
                {
                        heap = heaps[k];
                        if(SearchHeap(heap)) break;
                }
        }
        else
        {
                heap = GetProcessHeap();
                SearchHeap(heap);
        }
        HeapLock(heap);
        if(presentPtr != 0)
        {
                (*presentPtr) = (DWORD)hkPresent;            
        }
        if(resetPtr != 0)
        {
                (*resetPtr) = (DWORD)hkReset;
        }
        HeapUnlock(heap);
}

Этот метод делает довольно тривиальтую штуку - ищет в куче (Heap) процесса указатели на две нужных нам функции - Present и Reset. Ищет он при помощи метода SearchHeap, в котором нам интересна только пара деталей:


BOOL SearchHeap(HANDLE heap)
{
        int vtableLenBytes = sizeof(DWORD)*4;
        PROCESS_HEAP_ENTRY mem;
        mem.lpData = 0;
        while(HeapWalk(heap, &mem))
        {
                if(mem.wFlags == PROCESS_HEAP_UNCOMMITTED_RANGE) continue;
                DWORD* p = (DWORD*)mem.lpData;
                for(int i = 0; i < (int)(mem.cbData/sizeof(DWORD)); i++)
                {
                        if(memcmp(p, vtableFrag9, vtableLenBytes) == 0)
                        {
                                presentPtr = p + 11;
                                resetPtr = p + 10;
                                offsetPresent = *presentPtr;
                                offsetReset = *resetPtr;
                                g_D3D9_Present = (tPresent)((DWORD*)offsetPresent);
                                g_D3D9_Reset = (tReset)((DWORD*)offsetReset);
                                break;
                        }
                        p++;
                }
                if(presentPtr != 0) break;
        }
        return(presentPtr != 0);
}

Откуда взялись смещения 10 и 11? А всё просто. VMT - это таблица указателей, в которой указатели на функции всегда расположены в одинаковом порядке. Собственно, 10 и 11 - это порядковые номера функций.

У нас есть кусочек VMT, мы идём и ищем такой же кусочек в куче процесса. Как только нашли - отсчитываем от него 10 - это будет указатель на Reset, отсчитываем 11 - указатель на Present.

И всё, нужные указатели нашли, осталось только их поменять на свои. Этим будет заниматься HookVTableFunction:


PBYTE HookVTableFunction(PDWORD* dwVTable, PBYTE dwHook, INT Index)
{
    DWORD dwOld = 0;
    VirtualProtect((void*)((*dwVTable) + (Index*4)), 4, PAGE_EXECUTE_READWRITE, &dwOld);
    PBYTE pOrig = ((PBYTE)(*dwVTable)[Index]);
    (*dwVTable)[Index] = (DWORD)dwHook;
    VirtualProtect((void*)((*dwVTable) + (Index*4)), 4, dwOld, &dwOld);
    return pOrig;
}

Она принимает указатель на текущее устройство D3D, указатель на новую функцию и порядковый номер функции. Вы спросите, откуда же мы возьмём устройство D3D? А всё просто. В методе CheckAndHookPresent9 мы уже заменили указатели на оригинальные функции нашими собственными. Интересовать нас будет изменённая функция hkPresent:


HRESULT WINAPI hkPresent(LPDIRECT3DDEVICE9 pDevice, CONST RECT* src, CONST RECT* dest, HWND hWnd, LPVOID unused)
{    
        while(!npDevice)
        {
        npDevice = pDevice;
        }
        DrawIndicator(pDevice);
        return g_D3D9_Present(pDevice, src, dest, hWnd, unused);
}

Первыми же тремя строчками мы сохраняем себе адрес D3D-устройства, которое попыталось вызвать Present, но так как мы поменяли указатель в VMT - попало сюда. После этого мы можем спокойно использовать все его функции, будто это наша собственная программа, чем мы и пользуемся - вызываем DrawIndicator, который я рассмотрю чуть ниже. В общем, ясно, да? Получили устройство, установили все указатели на нужные нам.

Как выглядело раньше:

Игра - D3DDevice - VMT-Present - D3D9.DLL

Как выглядит теперь:

Игра - D3DDevice - VMT-hkPresent - hook.dll - D3D9.DLL

А вот и метод DrawIndicator, который банально рисует "менюшку":


void DrawIndicator(LPVOID self)
{
        IDirect3DDevice9* dev = (IDirect3DDevice9*)self;
        dev->BeginScene();
        D3DRECT rec = { 10, 10, 30, 30 };
        D3DCOLOR color = 0;
        if(indicator)
        {
                color = D3DCOLOR_XRGB(0, 255, 0);
        }
        else
        {
                color = D3DCOLOR_XRGB(255, 0, 0);
        }
        dev->Clear(1, &rec, D3DCLEAR_TARGET, color, 1.0f, 0);
        dev->EndScene();
}

Говорим, что из аргументов нам пришло устройство D3D, вызываем у него BeginScene(), чтобы порисовать, создаём квадрат 20х20 пикселей в верхнем левом углу экрана. Дальше смотрим, если indicator == true (VK_F1 и KeyboardHook все помнят?) - рисуем зелёным цветом, если false - то красным.

Рисуем мы, собственно, методом Clear() - просто очищаем выбранную область выбранным цветом, затем вызываем EndScene() и выходим.

Вот и результат, для нетерпеливых:

Оговорюсь, для запуска всего этого надо открыть игру в Cheat Engine, перейти в отладчик и найти там в меню "Tools - Inject DLL".


В принципе, вооружившись бумажкой и карандашом можно рисовать уже сейчас, но в следующем уроке (статье?) попробуем прикрутить сюда какой-нибудь шрифт. Доработок ещё уйма предстоит, этот способ работает не везде (например, у меня не работает в StarCraft II), да и много чего можно улучшить.

Такие вот дела. 200 строчек кода - а сколько веселья. :)