Показаны сообщения с ярлыком Code-Injection. Показать все сообщения
Показаны сообщения с ярлыком Code-Injection. Показать все сообщения

20140119

D3D: Трагически наступившее "скоро".

Всем привет!

У меня нашлось время и я решил ещё немного поковыряться с D3D и прочими неприличными словами. (:

Остановились мы на том, что написали перехватчик методов D3D, ответственных за рисование, и смогли нарисовать на экране чужого приложения квадратик. Попутно мы написали свой собственный инжектор DLL-ок.

Это всё, конечно, круто, но не очень сильно впечатляет и далековато от наших амбиций. Итак, встречаем:


Вот эта самая функция нам и понадобится. Я, честно сказать, фигово разбираюсь в 3д-графике и рисовании в целом, так что гуру этой области прошу меня сильно не пинать - объяснять буду, как понял сам.

Как мы помним, читали или где-нибудь слышали, нельзя просто так взять и нарисовать оружие, машину или нечто такое. Видеокарточки туповаты и рисовать умеют только примитивы - точки, линии, треугольники. Захотели квадрат - сложили два треугольника вместе. Эти штуки ещё иногда зовут полигонами, ага. Дык вот эта самая функция используется для отрисовки примитивов, если очень кратко. Нас там будет интересовать параметр BaseVertexIndex, в суть которого я тоже вник крайне смутно и пытаться объяснить это кому-то пока что не стану - давайте просто звать эту штуку индексом. Ещё пара крутых штук ждёт нас чуть дальше, когда я код приведу. Так как у меня возникли некоторые трудности с перехватом этой функции, то перехватывать мы её будем не вручную, а при помощи библиотечки detours от microsoft (все необходимые файлы идут в комплекте с исходниками), так что код слегка видоизменился. Вот, например:



Тут сразу бросаются в глаза последние три строчки - это, собственно, и есть перехват. Делает библиотека всё то же самое - мы скармливаем ей адрес найденной фунции, которую нужно перехватить, в её начало дописывается переход (JMP 00112233) на перехваченную функцию, а перехваченная выставляет всё, как было, отрабатывает и возвращает хук на место. Едем дальше:



Это - перехваченная DrawIndexedPrimitive, при помощи которой игра будет пытаться рисовать всё то, что мы с вами видим на экране. Я не буду вдаваться глубоко в подробности, нам это нужно один раз, так что приведу примерную суть:

1. Каждый вызов у нас есть некие данные (Stride и BaseVertexIndex) , которые нам надо узнать и запомнить для использования уже в чите. Они характеризуют тот или иной объект на экране.
2. Каждый раз, когда игра пытается что-нибудь нарисовать, мы заносим структуру этих данных в массив.
3. В логгере есть кнопки, позволяющие управлять процессом - менять значения Stride и BaseVertexIndex, выбранные на данный момент будут подсвечиваться зелёным цветом.
4. Задача - натыкать такое значение, чтобы нужный нам объект(-ты) подсветился(-лись) зелёным. Запомнить цифры.

Скомпилируется всё это дело в DLL, которую надо будет заинжектить в процесс игры (я не парюсь и продолжаю делать это через Cheat Engine). Поддерживаются любые игры, которые рисуют при помощи D3D9.

ВНИМАНИЕ тем, кто запускает игры из Steam (как я, например): В стиме в свойствах игры надо отрубить steam overlay ui, так как он работает точно так же - иначе получим вылет.

ВНИМАНИЕ тем, кто до сих пор не использует MS Visual Studio 2013 (как я, например): Если вам лень ставить 13-ю студию, то просто слейте все исходники и создайте новый проект (DLL). Ему понадобится в свойствах указать путь до include и lib для MS DirectX SDK (полгига которого можно спокойно скачать в гугле), если заругается на отсутствие detours.lib - она лежт в папке проекта.

Результат:


А вот и [ссылка] на все исходники. На днях будем учить этих инопланетных парней просвечивать сквозь стены. (:

20121028

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 строчек кода - а сколько веселья. :)










20120715

Продолжаем бороться с DMA.

Или встречайте [новый видеоурок]!

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

Ну и небольшую фишку по ассемблерным скриптам в СЕ тоже покажу.

20120229

20111225

Ломаем игры по заявкам читателей.

На этот раз ломаем закись азота (N2O) в игре Need For Speed: Carbon. Попутно узнаём ещё одну новую инструкцию по работе с дробными (float) числами. :)

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

20111217

Видеоурок по стратегиям.

Пока что, думаю, заключительный - начнём уже трейнеры писать, наконец. :)

Делать мы будем не абы чо, а Map Hack (!) - грозу всея онлайн-комьюнити, штуковину, позволяющую брать и бессовестно подглядывать за вражьим игроком, делая себе нифига себе такое преимущество.

Метод достаточно идиотский - наткнулся на него случайно, может работать не во всех играх. А может и работать, я во всех играх ещё не проверил. :)


PS: !!!Не пытайтесь!!! играть с этой штукой онлайн - велик риск быть забаненным. А я в этом не виноват, потому что сейчас это написал.

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

20111216

ВНЕЗАПНО, видеоурок!

По стратегиям, который я всё-таки записал, самым невероятным образом. Расскажу - не поверят!. Итак, опираясь на недоперевод статьи из предыдущих постов - переходим к наглядной практике:

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

Уже после записи урока хотел бы добавить пару слов - не обязательно лезть в дебри отладки и кода, когда можно чуток включить голову, ну правда. :)

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

И в очередной раз можно убедиться в исключительной полезности гугла и петабайт справочных материалов, по сети раскиданных. Встретили команду, но не знаете нафига она? Гугл! Забыли как правильно пишется какое-то слово? Гугл! Хотите научиться ломать игры? Г!.. Ан-нет, - keng. :)


20111214

Обломинго, товарищи!

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

Собственно, статья будет переводом одного зарубежного ресурса с моими небольшими дополнениями. Переводить (скорее даже - вольно пересказывать)  я буду основные моменты, т.к. в той статье использовались не совсем актуальные на данный момент инструменты, но суть способа передаётся отлично в любом случае. Итак:

-------------------------------------

Задача: Мгновенная постройка только для игрока.
Инструменты:

-Игра жанра RTS
-Сканер памяти (Cheat Engine)
-Отладчик (тоже Cheat Engine)
-Начальные навыки взлома игр - теория по DMA, инъекции кода

Теория:

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

Компьютерному же боту нафиг не сдалось ни на экран смотреть ни тем более звуки слушать - у него есть определённый набор условий и переменных, где хранятся необходимые данные (ресурсы, координаты построек\армии и всё прочее) - он их из памяти прочитает и отдаст алгоритмам команду действовать, исходя из условий.

Грубо говоря, для игрока всё видимое на экране - это дублирование внутриигровых значений, т.е. можно искать, скажем, ресурсы, а найти только адрес, значение которого выводит их на экран, но не будет истинным значением ресурсов. Как-то так. А вот у бота всё попроще - он сразу из памяти читает.

Практика:

Как вы все уже могли догадаться, первая половина взлома происходит достаточно просто:

1. Мы знаем, что постройка чего либо занимает определённое время. Начинаем строить какое-то здание, видим на экране отображение прогресса строительства - или полоску:

[|||||||||||||||||||||||||||||||=>75 %        ]

или просто здоровье здания\проценты в цифрах.

2. Возможных алгоритмов поиска будет аж три штуки. Вот они:
-Неизвестное значение, уменьшилось-уменьшилось-уменьшилось.
-Неизвестное значение, увеличилось-увеличилось-увеличилось.
-Неизвестное значение, изменилось-изменилось-изменилось.

Тип данных - а когда как, на самом деле. От целого 1 байт до с плавающей точкой.

3. Итак, ставим новое здание, пока строится - пробуем искать по описанным выше способам.

В конце концов находим адрес, вешаем на него брейкпоинт и смотрим отладчиком, какая инструкция туда пишет. Вот тут - очень важный момент:

Где-то рядом с этой инструкцией будет сравнение (человек \ бот) и затем - условный переход или call (вызов) функции. По сути, для бота алгоритм будет примерно таким:

-Проверить, не достроилось ли здание
-Если достроилось - сообщить об этом

А вот для человека - таким:

-Проверить, не достроилось ли здание
-Обновить экранное значение (полоску или проценты)
-Если достроилось - сообщить об этом

Вот после сравнения как раз и будет идти инструкция, нужно ли игре обновлять значение на экране. И бывает так, что эта штука - единственное, как можно отличить игрока-человека от бота.

Сама по себе инструкция постройки будет тоже простецкая, как-нибудь вот так:

cmp edi, ebx //сравниваем значение текущего прогресса с 100%
je done //если достроилось - выходим и сообщаем об этом игроку
sub edi, eax //достраиваем ещё кусочек

Т.е. мы просто берём и в инъекции вычитаем из edi его же значение - вуаля, всё будет строиться сразу же. Но перед этим нам необходимо будет проверить, человек ли строит, т.к. код-то один и тот же - ну, с парой отличий. :)

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

Как видите, всё достаточно просто. Самое полезное в этой теме - понимание теории про экранные значения, процесс поиска нужного нам адреса (просто так бывает и не догадаешься так искать) и (довольно часто) - новые способы сравнения игроков между собой. В остальном этот урок на 90% схож с темой one hit kill (убийство с одного выстрела), которую я показывал на примере Crimsonland.


PS: Я изо всех сил стараюсь выкроить время для нормальной записи урока и покажу всё это на примере - так информация воспримется лучше, но пока что буду обращать больше внимания на трейнеры и ассемблер, т.к. это правда займёт меньше времени. Спасибо всем, кто это прочитал, прочитает или даже поделится ссылкой с другом. Если захочется почитать оригинальную статью на английском, то её можно найти, к примеру, тут.


20111028

Восьмой, восьмой видеоурок!

Идём по [ссылке] и наслаждаемся. Качество может быть плохим какое-то время - видео только что загружено и всё ещё на обработке.

Наконец-то!

Ребята, записал, записал видеоурок моей мечты - огромный, здоров... А, хотя ладно, оставим это.

Записал видеоурок, посвящённый повадкам супермена - суперпрыжкам, но внезапно осознал, что опаздываю на работу. Работа пересилила, так что сейчас я в офисе, а урок выложу вечером, часов в шесть по московскому. Ждите.

20110929

Вторая часть седьмого видеоурока - делаем спидхак для Crimsonland!

С разбегу - [ссылка]!

Во второй части я покажу, как делается взлом скорости ("быстро бегать") для замечательной аркады Crimsonland, попутно объясню значение пары новых ассемблерных команд, характерных только для данного типа взлома. А именно:

fld [eax] - загружает значение типа float (с плавающей точкой) в [математический сопроцессор] (он же FPU) для последующих операций над ним.
fstp [eax] - соответственно, выгружает результат обратно в регистр eax.

Как именно вся эта фигня работает нам знать не настолько важно. Нам будут встречаться конструкции вида:

fld [eax+1234]
fadd [edi] (то же, что и add)
fsub [esx] (то же, что и sub)
fstp [eax+1234]


Основная мысль заключается в том, что главный процессор не умеет работать с дробными числами (float), так что для этого существует процессор поменьше - сопроцессор, который умеет только это. Командой fld (float load) мы заставляем главный процессор скормить нужное нам число сопроцессору, дальше в нём происходят какие-то действия над числом (fadd, fsub и т.п.), после чего по команде fstp число грузится обратно в главный процессор. Очень похоже на code-injection, правда? :)

Наша задача - или вклиниться в действия над значением после его загрузки в сопроцессор (пример - на видео), или посмотреть, что происходит чуть раньше - скажем, какая разница в координатах прибавляется\убавляется, часто можно обнаружить некоторый множитель. Типа, идём шагом - координаты умножаются на единичку, бежим бегом - умножаются на пять (бежим впятеро быстрее, чем идём шагом). Механизм действий схож для большинства игр, т.к. практически все хранят координаты в типе float, но ньюансы встречаются - нет универсального решения.

Теорию про сопроцессор и сопутствующие ему команды я выложу тут.



20110916

Шестой видеоурок, так сказать! :D

А если серьёзно - просто видеопояснение к [предыдущей записи] о Code-Injection. Ломаем таймер матча в Mortal Combat 4, а ещё - узнаём две новых команды ассемблера (совсем простеньких):

inc \ dec [регистр] - англ. increment (увеличение) \ decrement (уменьшение) - увеличивает или уменьшает содержимое [регистра] на единичку. Пример:

mov edi, 5 //кладём в edi пятёрочку
dec edi      //делаем магию!!

В итоге в edi будет лежать четыре. :)


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

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


Code-Injection: Подробности

В [пятом уроке] (и в четвёртом тоже ^^) мы рассматривали такую технику, как Code-Injection, или, если буквально, "инъекция кода". Давайте остановимся на этой штуке ещё немного - больно уж часто она используется. Больше скажу - одна из самых распостранённых техник взлома. :)

Итак. Представим, что имеется у нас вот такой гипотетический код игры:

Адрес   |  Команда      |    Комментарий
---------------------------------------------------------------------------------
0000000: mov eax, 3     //помещаем в регистр eax число "3"
0000001: sub eax, 1      //вычитаем из еах единичку
0000002: cmp eax, 0    //сравниваем еах с нулём
0000003: je "exit"         //если еах = 0, то выходим из игры
0000004: add eax, 2    //иначе прибавляем к еах двойку
0000005:
0000006:
0000007:
0000008:
0000009:
---------------------------------------------------------------------------------

Скажем, нашли мы нужный адрес в Cheat Engine, включили отладчик дабы посмотреть - а с чего это у нас заканчиваются жизни (которые, как вы поняли, в еах находятся), отладчик нам выплёвывает адрес 0х00000001.

Если мы хотим, чтобы жизни просто не убавлялись, мы заменяем команду "sub eax, 1" nop'ами - командами, которые ничего не делают. Почему я написал во множественном числе? Всё просто - одна команда "nop" занимает один байт места, а вот "sub eax, 1" места занимает чуток побольше. Точные значения нас сейчас не волнуют (их можно посмотреть в отладчике), скажем, что это четыре байта. Поэтому вместо:

sub eax, 1

Мы должны будем написать четыре команды "nop", по одной на каждый байт:

nop
nop
nop
nop

Если не соблюдать это правило (количество байт кода всегда неизменно) - получите синий экран и придётся делать заново.

Идём дальше. В чём суть техники Code-Injection? Мы берём какую-то команду (в данном примере - ту, что вычитает у нас жизни) и заменяем её на команду jmp - прыжок или переход. Там выполняем какой-то код, затем возвращаемся обратно - в код игры. Смотрите:

Адрес   |  Команда      |    Комментарий
---------------------------------------------------------------------------------

0000000: mov eax, 3             //помещаем в регистр eax число "3"
0000001: jmp [0000006]      //переходим ("прыгаем") на адрес 0x0000006
0000002: cmp eax, 0            //сравниваем еах с нулём
0000003: je "exit"                 //если еах = 0, то выходим из игры
0000004: add eax, 2            //иначе прибавляем к еах двойку
0000005:
0000006: add eax, 10 //добавляем к еах десять
0000007: jmp [0000002] //прыгаем обратно - на адрес 0x0000002
0000008:
0000009:
---------------------------------------------------------------------------------
В результате этих действий мы заставили игру выполнить наш код (находящийся под адресам 0х0000006-0х0000007) а затем вернуться обратно и продолжить свой. Что нам это даёт? На самом деле - что угодно, всё зависит от фантазии и навыков программирования. Можно добавить жизней, можно патронов, можно всех врагов убить, можно любимой бабушке привет  передать - в общем, чего душа пожелает, а дальше игра послушно вернётся к своим делам. :)

Почему делать нужно именно так? Потому что про количество байт и длину кода я писал не просто так - мы не можем уместить тысячу байт кода в размеры одной трёх-четырёхбайтовой команды - поэтому засовываем код в отдельный участок памяти, обращаемся к нему, а затем продолжаем исполнение игрового кода. Прелесть в том, что код всегда будет находиться в одном и том же месте - адреса меняться не будут. Такая вот загогулина. Использовать этот не слишком хитрый приём мы будем ещё очень много раз, так что понимание успеет придти. Если же нет - пишите, задавайте вопросы. :)