Программист, реверс-инженер
2,4
рейтинг
28 февраля 2012 в 13:13

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

В продолжение темы расширения функциональности готовых программ хотелось бы рассказать об ещё одном способе изменения логики работы уже скомпилированной программы, который не требует делать изменений в самом исполняемом файле. Это может пригодиться при распространении вашей модификации в США, где прямое вмешательство в исполняемый файл строго осуждается. Речь пойдёт о создании крошечной прокси-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
Евгений Врублевский @VEG
карма
351,0
рейтинг 2,4
Программист, реверс-инженер
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (32)

  • +1
    Статья хорошая, по делу и без буллшита.
    Толькот вот __declspec(naked) — чисто Microsoft-specific, в GCC например работать не будет.
    • +1
      И да, опечатка в статье — «играет тушь» :)
      • 0
        Спасибо, исправил.
    • 0
      Подозреваю что в gcc и *.def файлы работать не будут и много что ещё.
    • +3
      Да, я в начале статьи указал, что работа ведётся в Visual Studio 2010. К сожалению, в таких мелочах производители различных компиляторов не договорились между собой о единых стандартах.
      Будет очень полезно, если вы приведёте аналогичные по действиям участки кода для GCC :)
      • 0
        В GCC нет такой возможности (для х86), только через асм.

        С другой стороны, такие переходники лучше вообще сразу генерировать на асме (под фасм или ясм) автоматически.
  • +1
    Ух, спасибо за статью, интересно получилось. А главное ничего усложнённого излишне нет.
  • 0
    Круто, надо будет попробовать.
    Лично я добавлю еще возможность включать\выключать принудительное удерживание мыши в окне.
    • +1
      За это отвечает параметр UseCursorClip :)
  • 0
    Проверил на Героях III, работает, НО курсор мышки не соответствует действительности, т.е. бегает на пол окна и исчезает.
    • 0
      Поиграйтесь с параметрам UseCursor — должно помочь в случае, если игра берёт координаты от угла экрана, а не своего окна (что бывает часто).
      • 0
        Курсор заработал, но остались атефакты (диалог в правом углу)

        и так же и в игре, не инвентарь ни диалоги невидно полностью.
        • 0
          Возможно помогут какие-то другие параметры, либо уже нужно будет дополнительно немного поправить код в самом бинарнике. Как правило, там не сложные модификации.
    • +1
      Для Героев III лучше использовать HD mod ;)
  • +2
    Вопрос не программиста, но игрока: в теории это позволит играть в оконном режиме во всю «классику» — Arcanum, Baldur’s Gate, Fallout, Icewind Dale, Planescape?.. Это же прекрасно, спасибо вам большое!
    • +1
      По поводу Baldur's Gate: есть сборки все-в-одном, вроде Baldur's Gate Trilogy. Обычно туда включают www.gibberlings3.net/widescreen/, который позволяет или играть в окне, или подстроить игру под ваше разрешение экрана.

      • +1
        Забыл. Всем советую darktech.ucoz.ru/ эту сборку.
  • 0
    Замечательно! Задумывался о создании подобной библиотеки т.к. xsplit не поддерживает стрим при фулл скрине, а тут все готовенькое, спасибо.
  • 0
    Это очень круто, спасибо! Как раз была одна идейка, Ваша статья очень поможет.
  • 0
    А с новыми играми не пойдет? Попробовал со Steam CS 1.6. Вылетает ошибка при разных комбинациях параметров. Так и не удалось запустить.
    • 0
      CS 1.6 вроде как сама умеет в окне работать:
      тыц
      • 0
        Да это я в курсе, не дурак :) Интересует именно окно без рамки.
  • 0
    Спасибо, интересная статья. А не знаете, вот Detours для подобного использовать возможно?
    • 0
      Если вы пишете proxy-dll, то смысла дополнительно перехватывать проксируемые методы через Detours нет, поскольку вы и так можете писать свою реализацию этих методов. Если же вы хотите дополнительно отлавливать вызовы из других библиотек, то Detours здесь верный помощник.
  • +1
    Еще на тему прокси-dll есть полезная библиотечка Proxocket netresec.com/?b=1119573
    Подменяет Winsock и дампит весь трафик в файл.
  • 0
    Но надо отметить, что работать будет только с действительно старыми играми, которые используют ddraw. А более новые работают напрямую с D3D9!Direct3DCreate9, и там подменять придётся не столько функции, сколько целый интерфейс.
  • +2
    По материалам статьи набросал программку, генерирующую на основе выбранной библиотеки проект Visual Studio, формата 2008 либо 2010.

    Забирать здесь: Исполняемый файл / исходники (AutoIt).
  • 0
    Статья хорошая, но для Windows DLL такие чудеса — не новость. А вот кто-нибудь знает о подобном решении для Linux ELF? Причем не в принципе как сделать, это-то понятно, а именно готовое reusable решение — на изобретение всех велосипедов самому не хватит жизни. LD_PRELOAD не предлагать, интересует именно proxy решение.

    • 0
      А чем LD_PRELOAD не решение? Тут же подвох в том, что Windows ищет сначала в директории приложения, а потом уже в системных. В Linux такого нет, а значит и не получится без явного использования LD_PRELOAD.
      • 0
        Что значит не получится? Как может не получиться следующий финт: оригинальную libfoo.so переименованием в libfoo2.so, создаем proxy-so libfoo.so которая форвардит вызовы на libfoo2.so?

        Проблема с LD_PRELOAD в том, что он очень fragile: его нужно установить, кто угодно может его сбросить, он имеет ограничения по правам. Мой usecase: злые вендоры не хотят обновлять Android на пользовательских девайсах. Пользователи пытаются делать это сами. Корпорация добра Google подыгрвает вендорам, постоянно меняя API/ABI, в основном, именно ради изменений (ну там тип переименовали, C++ ABI). Основная проблема с интерфейсом EGL, связывающим вендоровский OpenGL и графическую подсистему Android. Пользователи хотят выразить свое отношение к происходящему написанием ABI-адаптера, чтобы OpenGL из предыдущих версий можно было использовать с новыми версиями Android. Причем парадигматически верно, чтобы решение всех этих гугло-вндоровских закавык было простое, как бублик и прозрачное, аки слеза, чтобы даже ребенок мог понять, как плохо стараются делать хорошо гугловендоры со своей стороны.

        ;-)
        • 0
          Заменить оригинальную, понятное дело, можно, но тогда эта подмена будет работать для всех программ её использующих, а не «избранных», как это делается в статье.
  • 0
    Возможно ли зуммировать игру в окне в X раз, в пропорции? Например, в 1.4 раза.
    Многие игры, поддерживающие только 640х480 смотрятся на больших экранах слишком мелко.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.