13 February 2009

Расширение функциональности готовых программ

AssemblerSystem ProgrammingReverse engineering
В мире ПО существует огромное количество программ, забытых своими разработчиками. Хорошо, когда уже есть хорошая альтернатива. А если ее нет? В программе может катастрофически не хватать каких-то мелочей, некоторые досадные ошибки могут годами доставлять массу неудобств пользователям, а на новых версиях ОС программа и вовсе может отказаться работать. Далеко не всегда имеются исходные коды, чтобы привести программу в порядок. Если программа простая — не составит труда за короткий срок создать альтернативу. Но если программа большая и сложная, что же делать в таком случае? Не всегда рационально тратить время и деньги на разработку полного аналога, ведь расширить в разумных рамках функциональность и исправить большинство ошибок можно уже в готовом исполняемом файле.
В этой статье будут продемонстрированы методики модификации исполняемых файлов на примере расширения функциональности легендарной игры Age of Empires II (стратегия реального времени).

Итак, что же мы имеем:

  • игра вышла в 1999 году и имеет статус abandonware — то есть она более не выставляется на продажу производителем и не приносит прибыли
  • последнее обновление выпущено в 2000 году, доработка не производится
  • игра является хорошим примером сложного ПО, полный аналог которого практически невозможно создать заново
  • имеются досадные ошибки, которые не были исправлены разработчиками

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

Отказ от ответственности


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

Сведения о «пациенте»


Исследуемая игра написана на движке Genie, разработкой которого с 1997 до 2000 года занималась компания Ensemble Studios. За это время на данном движке было выпущено 4 игры из серии Age of Empires. Далее Genie был лицензирован компании LucasArts, которая в 2001-2002 годах выпустила еще 2 игры на этом движке, но уже из серии Star Wars. К этому времени движок уже сильно устарел, поскольку работал в режиме 256 цветов, что очень скромно по сравнению с играми даже 2000 года. Именно поэтому доработка движка была остановлена, новых игр на его основе не выпускалось.
Игровой движок Genie загружает всю игровую графику, музыку, а главное — списки юнитов и их характеристики — из внешних файлов. То есть все игры на базе этого движка по большей части имеют очень похожие исполняемые файлы, и практически все модификации для одной игры без особых проблем портируются на другие.

Чего мы хотим добиться?


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

2. Добавить поддержку оконного режима
Age of Empires поддерживает 3 фиксированных разрешения: 800×600, 1024×768 и 1280×1024. С такими разрешениями картинка на широкоформатных мониторах выглядит растянутой, что доставляет дискомфорт. Добавить поддержку новых разрешений в игру можно, но так как в игре для каждого разрешения используются своя графика интерфейса, оставим это на потом, и ограничимся пока что добавлением оконного режима работы в игру.

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

4. Сделать игру переносимой
Игра не переносима. Для запуска на другом компьютере недостаточно скопировать игровые файлы, необходимо пройти весь процесс установки игры. В связи с широким распространением внешних накопителей хотелось бы наделить игру этим свойством.

5. Исправить известные ошибки
В интерфейсе игры имеются неприятные ошибки. Например, в диалоге подключения к IP игре в списке последних игр обрезаются ранее введенные адреса, что сводит удобство всей функции быстрого подключения на нет, приходится каждый раз вводить адрес сервера вручную.

Что нам для этого понадобится?


IDA Pro — интерактивный дизассемблер и отладчик, широко используется для реверс-инжиниринга. Умеет строить понятные даже новичкам блок-схемы. Вся его сила проявляется в интерактивном взаимодействии с пользователем. После автоматического анализа пользователь может давать функциям и переменным осмысленные имена, комментировать код и т.д. Достаточно немного понимать язык ассемблера x86 для того, чтобы начать заниматься исследованием программ прямо сейчас!

Flat Assembler — свободно распространяемый многопроходной ассемблер. FASM обладает небольшими размерами и очень высокой скоростью компиляции, имеет богатый и ёмкий макро-синтаксис, позволяющий автоматизировать множество рутинных задач. Одна из полезных возможностей — генерация чистого машинного кода без заголовков и т.д. Все это пригодится нам для генерации ассемблерных вставок.

Hexplorer — бесплатный HEX-редактор. Имеет простой дизассемблер x86, что позволяет удобнее ориентироваться внутри исполняемого файла. Используется непосредственно для ручной модификации двоичного файла без изменения его размера.

Итак, начнем.

Отключение проверки наличия CD


Найти код, который отвечает за проверку наличия CD, нам поможет IDA. Это оказалось очень просто сделать. Достаточно проследить вызовы функций GetDriveTypeA и GetVolumeInformationA, которые используются для получения информации о приводе, как мы обнаружим функцию проверки наличия CD по адресу 4485A0h (базовый адрес 400000h, то есть физически в файле машинный код находится по адресу 485A0h), которая при удачной проверке возвращает в eax единицу.
CheckCD         proc near
    sub     esp, 214h
    push    ebx
    push    esi
    ; ...
    ; множество проверок
    ; ...
    test    eax, eax
    jz cd_check_fail
cd_check_ok:
    pop     esi
    mov     al, 1
    pop     ebx
    add     esp, 214h
    retn    4
cd_check_fail:
    pop     esi
    pop     ebx
    add     esp, 214h
    retn    4
CheckCD         endp
При этом стоит заметить, что эта функция запрашивает значение CDPath из реестра, где хранится буква диска, с которого была установлена игра. То есть игра привязывается конкретно к той букве диска, с которого она была установлена, и при наличии оригинального диска в другом приводе игра все равно не запустится. Наиболее простое решение в данном случае — это заменить команду jz cd_check_fail, которая занимает 2 байта, на 2 операции nop, чтобы после проверки диска в eax всегда возвращалась единица. Однако, функция устроена таким образом, что при отсутсвии значения CDPath в реестре, функция сразу возвращает 0, что мешает переносимости программы. Поэтому всю функцию мы заменим на 3 простые команды:
use32
    xor	eax, eax
    inc	eax
    retn	4
Для получения машинных кодов этих команд нам пригодится FASM.
fasm src.asm dst.bin
Основной плюс этого ассемблера для нас сейчас — это то, что он позволяет генерировать чистый машинный код без заголовков. Получаем (по строчке на команду):
31 C0
40
C2 04 00
То есть нам нужно поместить эти 6 байт в начало функции определения наличия CD, а остальные команды заменить на nop (код 90h).
Исходный машинный код Полученный машинный код
8B 44 24 04 81 EC 14 02
00 00 85 C0 53 56 8B D9
...
33 С0 40 С2 04 00 90 90
90 90 90 90 90 90 90 90
...
В обычном HEX редакторе заменяем начиная с смещения 485A0h указанные выше машинные коды, после чего игра больше никогда не требует для запуска CD, даже если в реестре нет необходимого ключа. При этом мы освободили 288 байт для дополнительного машинного кода, что пригодится нам в дальнейшем.

Исправление ошибки


При подключении к сетевой игре по IP игра выводит список IP и названий игр, к которым игрок подключался ранее. Однако, по каким-то причинам IP адрес часто сохранялся не полностью, обрезалось несколько его последних символов. Необходимо разобраться в чем дело и исправить ошибку.
Займемся поиском кода, отвечающего за сохранение IP адресов последних игр в реестре. Нам известно, что игра сохраняет список последних IP игр в ключах с именами вида RecentGameName%d и RecentGameAddr%d. При помощи IDA находим по 3 ссылки на эти строки. Определить где нужный код очень просто — в коде со смещением 0512116h происходит запись этих ключей, в остальных случаях — считывание. Не будем приводить найденный участок машинного кода — там около 50 инструкций, общий смысл которых можно представить в виде маленького фрагмента кода на C.
for(int i = 0; i < count; i++)
{
	char* keyname;
	sprintf(keyname, "RecentGameName%d", i);
	RegSetVal(1, keyname, gamename[i], strlen(gamename[i]));
	sprintf(keyname, "RecentGameAddr%d", i);
	RegSetVal(1, keyname, gameaddr[i], strlen(gamename[i]));
}
Для установки значения в реестре игрой используется собственная функция-обертка RegSetVal(int, char* ValueName, char* Data, int DataSize) — название функции и параметров конечно же придуманы, в машинном коде их нет. Как видно, игра сохраняет в реестре ровно столько символов IP адреса, сколько их в названии сетевой игры. Судя по всему, программист скопировал 2 строчки сохранения названия игры для сохранения адреса, но при этом забыл исправить название массива в вызове strlen.
Для исправления ошибки у нас есть 2 варианта: подставить в strlen правильную строку или вместо strlen помещать константу 16 (максимальная длина строки адреса, например «255.255.255.255»).
Как же выглядит вызов strlen(gamename[i]) в машинном коде?
call    _sprintf ; +0x1022CF
mov     edi, ebp
or      ecx, 0FFFFFFFFh
xor     eax, eax
add     esp, 0Ch
repne scasb
not     ecx
push    ecx
Компилятор оптимизировал код и заменил вызов функции strlen() вычислением длины на месте. В предыдущем вызове strlen() использовалась та же строка, в вот опять оптимизация — указатель помещается в ebp один раз при первом вызове. То есть у нас нет драгоценных пары байт для того, чтобы поместить правильный указатель в ebp. Можно, конечно, сделать jmp за пределы функции, потом обратно — но это не самый красивый метод. Поэтому остановимся пока на самом простом варианте исправления ошибки — будем всегда сохранять 16 байт адреса. Для этого заменим приведенный выше машинный код на следующий:
Ассемблерный код Машинный код
call    _sprintf ; +0x1022CF
add     esp, 0Ch
mov     ecx, 0Fh
E8 CF 22 10 00
83 С4 0С
B9 0F 00 00 00

Поддержка файла конфигурации


Игра поддерживает множество полезных параметров командной строки, которые было бы удобно сохранить где-нибудь в файле, чтобы не передавать их каждый раз при запуске. Лучший способ реализовать подобное — это заставить игру при каждом старте загружать внешнюю библиотеку и выполнять функцию инициализации. При такой организации можно добавить множество полезных вещей в конфигурационные файлы, такие как: загрузка дополнительных библиотек (модулей) без необходимости модификации исполняемого файла, перехват функций работы с реестром для организации хранения всех игровых данных в конфигурационном файле игры, а не в реестре. Не лишним будет поддержка множества профилей с различными наборами настроек с возможностью быстрого их переключения. Итак, приступим.
Наиболее удобным местом подключения внешней библиотеки оказалась функция загрузки библиотеки ebueulax.dll, отвечающей за отображение лицензионного соглашения. Она как раз вызывается практически сразу после начала работы WinMain, однако помимо загрузки библиотеки ebueulax.dll и выполнения функции EBUEula она делает много лишних действий, поэтому мы полностью перепишем ее, чтобы она загружала библиотеку config.dll и выполняла библиотечную функцию LoadConfig с указателем на расположение командной строки в памяти (для её изменения) в качестве параметра.
Ассемблерный код Машинный код
LoadConfigLib   proc near
    sub     esp, 208h
    push    esi
    push    edi
    push    ebx
    push    ecx
    push    edx
    mov     esi, ds:LoadLibraryA
    push    67B8C4h; lpLibFileName
    call    esi ; LoadLibraryA
    mov     ebx, eax
    test    ebx, ebx
    jz      short loc_587443
    push    offset aLoadconfig
    push    ebx    ; hModule
    call    ds:GetProcAddress
    mov     edi, eax
    test    edi, edi
    jz      short loc_58743C
    mov     eax, [esp+220h];**args
    push    eax
    call    edi
    jmp     short loc_587443
loc_58743C:
    push    ebx  ; hLibModule
    call    ds:FreeLibrary
loc_587443:
    pop     edx
    pop     ecx
    pop     ebx
    pop     edi
    pop     esi
    add     esp, 208h
    retn
LoadConfigLib   endp
 
81 EC 08 02 00 00
56
57
53
51
52
8B 35 E0 51 63 00
68 C4 B8 67 00
FF D6
8B D8
85 DB
74 25
68 B8 B8 67 00
53
FF 15 C8 50 63 00
8B F8
85 FF
74 0C
8B 84 24 20 02 00 00
50
FF D7
EB 07

53
FF 15 20 51 63 00

5A
59
5B
5F
5E
81 C4 08 02 00 00
C3
Приведенный код делает несколько вызовов библиотечных функций. Их адреса были подставлены вручную в машинный код. В следующей статье (если вы не будете против :) ) я постараюсь рассказать как при помощи FASM можно генерировать код, полностью готовый к вставке в исполняемый файл.
Заменяем исходную функцию (начинается со смещения 187400h) новой при помощи обычного HEX-редактора. Поскольку новая функция значительно короче исходной, не забываем заменить оставшиеся байты кодом операции nop — 90h. Мы получили еще 192 байта свободных байта для дополнительного машинного кода. Это место можно будет использовать в дальнейшем.
Исходный машинный код Полученный машинный код
81 EC 08 02 00 00 53 56 
8B 35 E0 51 63 00 57 68
CC B8 67 00 FF D6 8B D8 
85 DB 75 0A 5F 5E 5B 81
C4 08 02 00 00 C3 68 C4 
B8 67 00 53 FF 15 C8 50
63 00 8B F8 85 FF 75 13 
53 FF 15 20 51 63 00 5F
5E 33 C0 5B 81 C4 08 02 
00 00 C3 8B 84 24 18 02
00 00 50 FF D6 8B F0 85
...
81 EC 08 02 00 00 56 57
53 51 52 8B 35 E0 51 63
00 68 C4 B8 67 00 FF D6 
8B D8 85 DB 74 25 68 B8
B8 67 00 53 FF 15 C8 50 
63 00 8B F8 85 FF 74 0C
8B 84 24 20 02 00 00 50 
FF D7 EB 07 53 FF 15 20
51 63 00 5A 59 5B 5F 5E 
81 C4 08 02 00 00 C3 90
90 90 90 90 90 90 90 90 
...
Всю остальную работу берет на себя модуль config.dll. Его можно написать на C/C++/Delphi. Его задача проста — принимать от исполняемого файла указатель на командную строку приложения и в зависимости от настроек в конфигурационном файле изменять ее. Как бонус — нам пригодится возможность подгружать дополнительные модули, настраивая это в конфигурационном файле. Для этого нам нужно просто вызывать WinAPI функцию LoadLibrary именем файла модуля, которое необходимо загрузить в адресное пространство игры.

Поддержка оконного режима


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

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

Сам по себе модуль wndmode.dll является сильно модифицированной версией библиотеки d3dhook.dll, где реализовано:

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

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

Что получилось?


Работа по модификации проделана большая, здесь мы рассмотрели лишь некоторые изменения, поскольку описание даже небольших изменений требует огромных усилий. Остальные модификации при желании вы сможете исследовать в IDA, скачав готовый модифицированный файл.

В целом удалось добиться таких результатов:

  • Вместо ebueulax.dll (отображение лицензии) загружается config.dll (поддержка конфигурационных файлов)
  • Оконный режим работы (модуль wndmode.dll — модифицированный d3dhook.dll)
  • Код функций работы с курсором адаптирован для работы в оконном режиме (решает много неприятных проблем, вызванных тем, что игра не рассчитана на работу в окне)
  • Игра корректно работает без ее предварительной установки (достаточно скопировать файлы игры), для игры не нужен CD
  • Исправлена ошибка оригинальной игры, когда при игре по IP в списке последних игр обрезались IP адреса
  • Изменен порядок и расположение кнопок в меню «Один игрок» и «Редактор», исправлена ошибка с выделением кнопок в меню «Один игрок» (переписан внушительный участок кода)
  • Исполняемый файл может запускаться из любого подкаталога, а не только из age2_x1 или корня (практически полностью переписано начало процедуры WinMain)
  • Нет проверки наличия файла empires2.exe (его можно удалить)
  • При включенном параметре MIDIMUSIC после сворачивания мелодия начинается заново (не сбрасываются инструменты)

Все изменения работают стабильно, никаких сбоев не обнаружено. В оконном режиме осталась пока что нерешенной проблема отображения системного курсора одновременно с игровым после того, как он покинет область игрового окна и вернется назад. Однако, это вызвано лишь только тем, что игровой движок не рассчитан на запуск в окне и не предусматривает вариант, когда курсор покинет игровое окно. В ближайшей версии модификации это будет исправлено.
Логическим завершением развития данной модификации может стать внедрение в игру поддержки игровых комнат с созданием соответсвующего игрового сервера, поскольку официальные игровые комнаты Age of Empires на Zone были закрыты ещё несколько лет назад. Всё это вполне возможно реализовать, не имея при этом на руках никаких исходных кодов.

Заключение


Если вам интересно попробовать в действии то, что у меня получилось, можете скачать Age of Empires II: The Conquerors (Lite Edition) (всего 94МБ) с моего сайта.
Простите, что здесь написано обо всем и сразу, и не сильно подробно. Я старался описать общую схему. В следующих статьях можно рассматривать каждый этап модификации в отдельности, давать какие-то советы и т.д.
Tags:age of empiresasmreverse engineeringисследование программexe modмодификация исполняемых файлов
Hubs: Assembler System Programming Reverse engineering
+202
35.6k 368
Comments 120