Pull to refresh

Создание прокси-dll для запуска DirectDraw игр в окне

Reading time 5 min
Views 19K
В продолжение темы расширения функциональности готовых программ хотелось бы рассказать об ещё одном способе изменения логики работы уже скомпилированной программы, который не требует делать изменений в самом исполняемом файле. Это может пригодиться при распространении вашей модификации в США, где прямое вмешательство в исполняемый файл строго осуждается. Речь пойдёт о создании крошечной прокси-dll (всего ≈4 килобайта) для подмены используемой приложением библиотеки на примере ddraw.dll.

Приступим


Вся работа будет проводиться в Visual Studio 2010. Для начала исследуем целевую библиотеку на предмет экспортируемых функций. Для этого воспользуемся утилитой dumpbin (в каталоге VS2010\VC\bin):
vcvars32
dumpbin /EXPORTS c:\windows\system32\ddraw.dll
В результате получим:
    ordinal hint RVA      name
          1    0 00002E69 AcquireDDThreadLock
          2    1 000327FA CompleteCreateSysmemSurface
          3    2 00032FAE D3DParseUnknownCommand
          4    3 00033EEF DDGetAttachedSurfaceLcl
          5    4 000325D7 DDInternalLock
          6    5 0003258C DDInternalUnlock
          7    6 000363FC DSoundHelp
          8    7 0000859D DirectDrawCreate
          9    8 00037851 DirectDrawCreateClipper
         10    9 0000EBC6 DirectDrawCreateEx
         11    A 000338C9 DirectDrawEnumerateA
         12    B 00033368 DirectDrawEnumerateExA
         13    C 00032CB2 DirectDrawEnumerateExW
         14    D 0003333B DirectDrawEnumerateW
         15    E 000387C1 DllCanUnloadNow
         16    F 00038607 DllGetClassObject
         17   10 00032675 GetDDSurfaceLocal
         18   11 0003A5F9 GetOLEThunkData
         19   12 0000E927 GetSurfaceFromDC
         20   13 00027CC4 RegisterSpecialCase
         21   14 00002EA8 ReleaseDDThreadLock
         22   15 000421A6 SetAppCompatData

На основании этого мы уже можем создавать свою прокси-dll. Поскольку ничего кроме WinAPI нам не нужно, поэтому в свежем проекте win32-библиотеки первым делом отключим RTL, включив параметр /NODEFAULTLIB и указав точку входа DllMain. Это позволит сильно выиграть в объёме. Далее в DEF файле укажем экспортируемые функции в следующем виде:

LIBRARY "ddraw"
EXPORTS
AcquireDDThreadLock           = FakeAcquireDDThreadLock            @1
CheckFullscreen               = FakeCheckFullscreen                @2
CompleteCreateSysmemSurface   = FakeCompleteCreateSysmemSurface    @3
D3DParseUnknownCommand        = FakeD3DParseUnknownCommand         @4
...

Поскольку нам необходимо добиться, чтобы наши «фейковые» функции просто передавали управление своим оригиналам, в C++ объявление каждой из них будет выглядеть так:
__declspec(naked) void FakeAcquireDDThreadLock()
{
    _asm { jmp [ddraw.AcquireDDThreadLock] }
}
Используя __declspec(naked) мы заставляем компилятор не генерировать код стандартного пролога и эпилога, которые вставляют данные в стек. Как результат — данные в стеке хранятся уже в подходящем виде, и мы можем просто передать управление оригинальной функции одной командой jmp без повторной передачи параметров в стек.

Адрес для перехода берётся из структуры ddraw, которая выглядит следующим образом:
struct ddraw_dll
{
	HMODULE dll;
	FARPROC	AcquireDDThreadLock;
	FARPROC	CheckFullscreen;
	FARPROC	CompleteCreateSysmemSurface;
	FARPROC	D3DParseUnknownCommand;
	// ... аналогично объявляются другие функции ...
} ddraw;
Эта структура заполняется в DllMain в момент загрузки нашей прокси-dll. Сперва мы загружаем оригинальную библиотеку, затем по очереди получаем адреса всех экспортируемых функций. Код будет выглядеть примерно так:
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
    char path[MAX_PATH];
    switch (ul_reason_for_call)
    {
        case DLL_PROCESS_ATTACH:
            CopyMemory(path+GetSystemDirectory(path,MAX_PATH-10), "\\ddraw.dll",11);
            ddraw.dll = LoadLibrary(path);
            if (ddraw.dll == false)
            {
                MessageBox(0, "Cannot load original ddraw.dll library", APP_NAME, MB_ICONERROR);
                ExitProcess(0);
            }
            ddraw.AcquireDDThreadLock         = GetProcAddress(ddraw.dll, "AcquireDDThreadLock");
            ddraw.CheckFullscreen             = GetProcAddress(ddraw.dll, "CheckFullscreen");
            ddraw.CompleteCreateSysmemSurface = GetProcAddress(ddraw.dll, "CompleteCreateSysmemSurface");
            ddraw.D3DParseUnknownCommand      = GetProcAddress(ddraw.dll, "D3DParseUnknownCommand");
            // ... аналогичные вызовы для других экспортируемых функций ...
        break;
        case DLL_PROCESS_DETACH:
            FreeLibrary(ddraw.dll);
        break;
    }
    return TRUE;
}

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

Оконный режим для DirectDraw игр


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

Однако, это слишком большой объём работы для демонстрационной работы, и поэтому мы просто воспользуемся услугами библиотеки wndmode.dll из предыдущей статьи, которая уже реализует всё необходимое, достаточно лишь только подгрузить её в адресное пространство процесса.

Сделаем это прямо в DllMain, добавив в case DLL_PROCESS_ATTACH одну строку:
LoadLibrary("wndmode.dll");
Остальные изменения на ваш вкус :)

Библиотека wndmode.dll


Эта библиотека занимается тем, что перехватывает все вызовы DirectDraw, изменяет параметры таким образом, чтобы программа работала в окне, и только после этого передаёт управление оригинальным функциям DirectDraw.

Сам по себе wndmode.dll является сильно модифицированной версией библиотеки d3dhook.dll, где реализовано:
  • Полная независимость от программы D3D Windower
  • Настройки загружаются из секции [WINDOWMODE] файла wndmode.ini
  • Настройки по умолчанию заменены для совместимости с Genie
  • Добавлен параметр Border, который включает/выключает рамку вокруг окна
  • Если игровое разрешение равно системному, автоматически убирается рамка
То есть при помощи wndmode.dll мы можем попробовать практически любую DirectDraw игру сделать оконной.

В wndmode.ini могут содержаться следующие настройки:
[WINDOWMODE]
UseWindowMode=1
UseGDI=0
UseDirect3D=0
UseDirectInput=0
UseDirectDraw=1
UseDDrawColorEmulate=1
UseDDrawFlipBlt=0
UseDDrawColorConvert=1
UseDDrawPrimaryBlt=1
UseDDrawAutoBlt=0
UseDDrawEmulate=0
UseDDrawPrimaryLost=0
UseCursorMsg=0
UseCursorSet=0
UseCursorGet=0
UseSpeedHack=0
SpeedHackMultiple=10
UseBackgroundResize=0
UseForegroundControl=0
UseFGCGetActiveWindow=0
UseFGCGetForegroundWindow=0
UseFGCFixedWindowPosition=0
EnableExtraKey=0
ShowFps=0
UseCursorClip=0
UseBackgroundPriority=0
DDrawBltWait=-1
Border=1

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

Скачать


Исходный код: на bitbucket.org
Бинарники: wndmode.zip (340 кб)

Демо: Age of Empires: The Rise of Rome


Копируем наш ddraw.dll, wndmode.dll и wndmode.ini в каталог с игрой, запускаем игру. Барабанная дробь…

image
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+65
Comments 32
Comments Comments 32

Articles