Pull to refresh

Взлом Age of Mythology: отключение тумана войны

Reading time16 min
Views15K
Original author: Alex Abramov

Введение


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





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





Целью этого поста будет создание открывающего всю карту хака, дающего игроку значительное преимущество. Он позволит игроку видеть, что делают противники на карте, и выбирать наилучшее время и место для атаки. Этот хак будет разработан для оригинальной версии игры, но позже я покажу, как применить его к новой, расширенной версии, которая сейчас распространяется на Steam.

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

CRC32: 7F1AF498
MD5: 09876F130D02AE760A6B06CE6A9C92DB
SHA-1: AAAC9CD38B51BEB3D29930D13A87C191ABF9CAD4

Часть первая: трудный способ


Начинаем работу


Наша цель — разработка того, что откроет игроку все данные карты, давая игроку полную информацию о том, что происходит в игре. Хорошая новость: раскрытие и сокрытие карты — это встроенный функционал игры. Игра поддерживает воспроизведение записанных игр, и опция раскрытия-сокрытия карты является частью UI этой функции.



Кнопка «Fog of War» управляет раскрытием карты и возвратом в нормальное состояние, в котором игрок видит только то, что исследовал. План заключается в том, чтобы найти обработчик этой кнопки и трассировать местоположение логики раскрытия карты. Найдя её, мы просто выполним инъекцию DLL в процесс игры, чтобы вызывать функцию раскрытия карты. Для этой работы подходит такой инструмент, как Cheat Engine, который полезен исследования и изменения памяти, отладки, дизассемблирования и других операций в контексте взлома игр. В этой статье я не буду описывать работу с этим инструментом, для этого есть множество других ресурсов.

После запуска и подключения Cheat Engine вопрос заключается в том, где находится код, взаимодействующий с кнопкой. Простейший способ выяснить это — применить стандартные практики программирования. В частности, активная кнопка будет иметь где-то в памяти значение 1, а неактивная — значение 0. Поэтому это становится вопросом тестирования и терпения. Поиск в памяти процесса значения «1» (когда кнопка активна) вернул 337 597 результатов. Если вы попробуете сделать то же самое, не ожидайте, что значения будут такими же.



Это слишком много для проверки. Нажмём кнопку снова, чтобы она стала неактивной, и выполним поиск значения «0». Программа вернёт 376 — всё ещё слишком много.



Повтор этого процесса ещё несколько раз сократила область поиска до уже вполне удобного 21 адреса.



20 из этих 21 были очень близки друг к другу. 0x08FC71A4 казался в этом ряду исключением. Изучив его внимательнее и изменив значение «0», удалось переключить кнопку в неактивное состояние. Итак, мы нашли нужный адрес, и 20 остальных можно спокойно отбросить. Следующий шаг — выяснить, что же его записывает.



На этом этапе Cheat Engine подключает отладчик и отслеживает все операции записи в 0x08FC71A4. После нескольких нажатий кнопки были выявлены следующие команды. Эти команды выполняли запись в 0x08FC71A4.



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



и возня с игрой позволили выяснить, что эта функция вызывается для каждой кнопки. Здесь ECX — указатель на кнопку, а +0x1A4, вероятно, содержит свойство IsToggled, которое присваивает соответствующее состояние кнопки. Это присваивание выполняется во второй команде записи, где EDX может быть «0» (неактивная) или «1» (активная). Код может казаться немного сложным, но он всего лишь проверяет, что включенное состояние является правильным, а затем устанавливает свойство IsToggled перед вызовом функции и возвратом.

Адрес получателя +0x14B670 — это тоже код, относящийся ко всем кнопкам. Здесь нам нужно медленно обойти всё и найти области кода, которые могут относиться к кнопке «Fog of War». Можно применить разные подходы, но я обычно пользуюсь следующим:

  • Адреса вызовов, вычисляемые через регистр. Это может означать механизм обработки события, выполняемый после изменения состояния кнопки, что-нибудь вроде OnChanged/OnEnabled/OnDisabled или похожая функция.
  • Параметры функций, являющиеся указателями на функции.
  • Вызовы функций, которые получают аргументы 1 или 0.

Шаг с заходом в +0x14B670 даёт нам следующий (частичный) ассемблерный код, представленный ниже. В ассемблерном коде указываются абсолютные адреса, а не адрес начала модуля в памяти + смещение, потому что их гораздо проще скопировать из IDA, чем из Cheat Engine.

.text:0054B670 mov eax, large fs:0
.text:0054B676 push 0FFFFFFFFh
.text:0054B678 push offset SEH_54B670
.text:0054B67D push eax
.text:0054B67E mov large fs:0, esp
.text:0054B685 sub esp, 8
.text:0054B688 push esi
.text:0054B689 mov esi, ecx
.text:0054B68B mov eax, [esi+148h]
.text:0054B691 push edi
.text:0054B692 mov edi, [esi]
.text:0054B694 push eax
.text:0054B695 push esi
.text:0054B696 lea ecx, [esp+24h+var_10]
.text:0054B69A call sub_4D7470
.text:0054B69F mov ecx, [eax]
.text:0054B6A1 push ecx
.text:0054B6A2 push 1
.text:0054B6A4 mov ecx, esi
.text:0054B6A6 call dword ptr [edi+54h]
.text:0054B6A9 cmp [esp+1Ch+arg_0], 0Dh
.text:0054B6AE jnz loc_54B769
.text:0054B6B4 lea edi, [esi+154h]
...


Выяснилось, что вызов с заходом в 0x004D7470 (красный) возвращается довольно быстро, поэтому здесь он не будет показан. Следующий вызов (синий) в +0x14B6A6 выполняет вызов через регистр. Это хороший кандидат на внимательное изучение. Эта функция может вызывать два возможных адреса:

...
.text:0054BF98 push 0Ch
.text:0054BF9A call dword ptr [eax+0CCh]
.text:0054BFA0
.text:0054BFA0 loc_54BFA0: ; CODE XREF: sub_54BF80+Fj
.text:0054BFA0 ; sub_54BF80+14j
.text:0054BFA0 mov ecx, [esp+0Ch+arg_8]
.text:0054BFA4 push ecx
.text:0054BFA5 push edi
.text:0054BFA6 push ebx
.text:0054BFA7 mov ecx, esi
.text:0054BFA9 call sub_4D4EF0
.text:0054BFAE pop edi
...


Команда по адресу +0x14BF9A (красный) никогда не вызывается при отладке и проходе, поэтому нет никакого смысла изучать её. Для исследования остаётся только следующий вызов в +0x14BFA9 (синий). Эта функция оказалась очень большой в размерах и имеет широкое ветвление с множеством возможных мест вызова. С помощью отладки бОльшую часть этой логики можно пропустить. Трассировкой кода, который выполняется только при активной кнопке «Fog of War», мы выделяем всего три места вызова.

...
.text:004D504C cmp esi, dword_A9D068
.text:004D5052 jz short loc_4D5087
.text:004D5054 push esi
.text:004D5055 call sub_424750
.text:004D505A mov edi, eax
.text:004D505C add esp, 4
.text:004D505F test edi, edi
.text:004D5061 jz short loc_4D5070
.text:004D5063 push esi
.text:004D5064 call sub_4D58B0
.text:004D5069 add esp, 4
.text:004D506C test edi, edi
.text:004D506E jnz short loc_4D5079
.text:004D5070
.text:004D5070 loc_4D5070: ; CODE XREF: sub_4D4EF0+171j
.text:004D5070 pop edi
.text:004D5071 pop esi
.text:004D5072 pop ebp
.text:004D5073 xor al, al
.text:004D5075 pop ebx
.text:004D5076 retn 0Ch
.text:004D5079 ; ---------------------------------------------------------------------------
.text:004D5079
.text:004D5079 loc_4D5079: ; CODE XREF: sub_4D4EF0+17Ej
.text:004D5079 mov eax, [esp+10h+arg_4]
.text:004D507D mov edx, [edi]
.text:004D507F push ebp
.text:004D5080 push eax
.text:004D5081 push ebx
.text:004D5082 mov ecx, edi
.text:004D5084 call dword ptr [edx+54h]
.text:004D5087
.text:004D5087 loc_4D5087: ; CODE XREF: sub_4D4EF0+157j
.text:004D5087 ; sub_4D4EF0+162j
.text:004D5087 pop edi
...


Вызов в +0xD5055 (красный) после трассировки приводит в тупик. То же относится и к +0xD5064 (оранжевый). Если зайти в них с помощью отладчика и начать трассировать путь выполнения кода, то оказывается, что эти две функции имеют очень схожее поведение. Однако, ничто не говорит о том, что они имеют что-то общее с работой кнопки «Fog of War» в отношении взаимодействия с картой. Установка контрольной точки на этих двух командах показывает, что они постоянно откуда-то вызываются и выполняют логику только вызывающего объекта. На этом этапе мы всё ещё находимся в общем коде, относящемся к UI и нажатиям на кнопки, поэтому достаточно безопасно посчитать, что эти две функции не имеют ничего общего с раскрытием карты.

Последнее место вызова — это +0xD5084 (синий). Заход в него ведёт к +0xD4EF0, который является ещё одной большой функцией.

.text:004D4EF0 push ebx
.text:004D4EF1 mov ebx, [esp+4+arg_0]
.text:004D4EF5 push ebp
.text:004D4EF6 mov ebp, [esp+8+arg_8]
.text:004D4EFA push esi
.text:004D4EFB mov esi, ecx
.text:004D4EFD mov ecx, [esi+0B8h]
...


Если поставить в ней контрольную точку, то она срабатывает постоянно, то есть она тоже является стандартным кодом обработки. Если пройти дальше, то можно увидеть, что она возвращается к коду, представленному в предыдущем листинге. Будут выполнены те же два вызова к 0x00424750 и 0x004D58B0. Затем выполняется вызов в [EDX+0x54], но в этот раз EDX будет иметь другое значение. В этом втором вызове это приводит к следующей функции по адресу +0xD0C70:

.text:004D0C70 mov ecx, [ecx+14Ch]
.text:004D0C76 test ecx, ecx
.text:004D0C78 jz short loc_4D0C91
.text:004D0C7A mov edx, [esp+arg_8]
.text:004D0C7E mov eax, [ecx]
.text:004D0C80 push edx
.text:004D0C81 mov edx, [esp+4+arg_4]
.text:004D0C85 push edx
.text:004D0C86 mov edx, [esp+8+arg_0]
.text:004D0C8A push edx
.text:004D0C8B call dword ptr [eax+30h]
.text:004D0C8E retn 0Ch
.text:004D0C91 ; ---------------------------------------------------------------------------
.text:004D0C91
.text:004D0C91 loc_4D0C91: ; CODE XREF: sub_4D0C70+8j
.text:004D0C91 xor al, al
.text:004D0C93 retn 0Ch
.text:004D0C93 sub_4D0C70 endp


Здесь только одно реальное место вызова, поэтому эту функцию довольно просто проанализировать. Установка контрольной точки показывает, что её вызов выполняется отовсюду, то есть это общий код. Вызов [EAX+0x30] ведёт к +0x680D0. Повтор процесса с контрольными точками показывает, что её всё равно вызывают отовсюду, поэтому здесь нет ничего полезного.

.text:004680D0 push 0FFFFFFFFh
.text:004680D2 push offset SEH_4680D0
.text:004680D7 mov eax, large fs:0
.text:004680DD push eax
.text:004680DE mov large fs:0, esp
.text:004680E5 sub esp, 0F8h
.text:004680EB mov eax, [esp+104h+arg_8]
.text:004680F2 push ebx
.text:004680F3 push ebp
.text:004680F4 push esi
.text:004680F5 mov esi, [esp+110h+arg_0]
.text:004680FC push edi
.text:004680FD mov ebp, ecx
.text:004680FF mov ecx, [esp+114h+arg_4]
.text:00468106 push eax
.text:00468107 push ecx
.text:00468108 push esi
.text:00468109 mov ecx, ebp
.text:0046810B mov [esp+120h+var_F0], ebp
.text:0046810F call sub_4718B0
.text:00468114 test al, al
...


Поиск специфического кода


Шаг с заходом в первое место вызова по адресу +0x6810F переносит нас к функции, содержащей огромную таблицу переходов (на скриншоте ниже). Это может быть многообещающим указанием на то, что мы нашли область, управляющую событиями или выполняющую механизмы обработки событий.



Пошаговое выполнение кода приводит нас к следующему case:

.text:00471DB4 loc_471DB4: ; CODE XREF: sub_4718B0+4FDj
.text:00471DB4 ; DATA XREF: .text:off_471FA0o
.text:00471DB4 push edi ; jumptable 00471DAD case 4
.text:00471DB5 call sub_54E7D0
.text:00471DBA mov esi, eax
.text:00471DBC add esp, 4
.text:00471DBF test esi, esi
.text:00471DC1 jz loc_471F5F ; jumptable 00471DAD case 3
.text:00471DC7 push edi
.text:00471DC8 call sub_4D58B0
.text:00471DCD add esp, 4
.text:00471DD0 test esi, esi
.text:00471DD2 jz loc_471F5F ; jumptable 00471DAD case 3
.text:00471DD8 mov edx, [esi+1A4h]
.text:00471DDE mov ecx, [esp+50h+var_40]
.text:00471DE2 cmp edx, ebx
.text:00471DE4 setz al
.text:00471DE7 push eax

.text:00471DE8 call sub_58EA10
.text:00471DED mov al, 1
.text:00471DEF jmp loc_471F65
...


Установив контрольную точку на +0x71DB4 (розовый) и продолжив, мы выяснили, что больше сюда ничего постоянно не попадает. При нажатии на кнопку «Fog of War» мы видим, что выполняется +0x71DB4. И, наконец, после длительной трассировки мы получаем свидетельства о том, что находимся внутри кода, относящегося к кнопке «Fog of War». Первая команда вызова находится в +0x71DB5 (красный). Эта функция получает через EDI один параметр, и это всегда постоянное значение. Выполняя пошагово код и внимательно наблюдая за значениями всех параметров или адресуемых/загружаемых адресов, мы не находим ничего, что говорит об значения переключения кнопки. В частности, нажимая на кнопку раскрытия-сокрытия карты и трассируя функцию, мы не нашли ничего изменяющегося, поэтому исключили её. Команда по адресу +0x71DC8 (оранжевый) вызывает адрес 0x004D58B0, который мы уже исследовали выше. То же самое происходит и с функцией. Она всегда получает то же значение, что и предыдущая функция и ничего не говорит о том, что она записывает значение переключения или управляет кодом, основанным на этом значении.

Следующий вызов находится по адресу +0x71DE8. Эта функция тоже получает один параметр и это тоже последняя функция, вызываемая перед выходом из функции обработки таблицы переходов. В бирюзовом блоке есть очень интересный код. Значение загружается из [ESI+0x1A4], затем сравнивается с EBX. Результат этого сравнения присваивает байту AL значение 0 или 1. EAX, который будет равен 0 или 1, затем передаётся как аргумент функции по адресу 0x0058EA10. Нажатие кнопки в игре и пошаговое выполнение показывают, что EBX всегда содержит значение 1, а EDX содержит 0 или 1, в зависимости от того, скрыта или раскрыта карта. Можно предположить, что это та функция, которая используется для раскрытия и сокрытия карты. Ассемблерный листинг для 0x0058EA10 показан ниже:

.text:0058EA10 sub_58EA10 proc near ; CODE XREF: sub_4718B0+538p
.text:0058EA10 ; sub_58DF30+919p ...
.text:0058EA10
.text:0058EA10 arg_0 = dword ptr 4
.text:0058EA10
.text:0058EA10 push ebx
.text:0058EA11 mov ebx, [esp+4+arg_0]
.text:0058EA15 mov [ecx+53h], bl
.text:0058EA18 mov eax, dword_A9D244
.text:0058EA1D mov ecx, [eax+140h]
.text:0058EA23 test ecx, ecx
.text:0058EA25 jz short loc_58EA43
.text:0058EA27 push 1
.text:0058EA29 push ebx
.text:0058EA2A call sub_5316B0

.text:0058EA2F mov ecx, dword_A9D244
.text:0058EA35 mov ecx, [ecx+140h]
.text:0058EA3B push 1
.text:0058EA3D push ebx
.text:0058EA3E call sub_5316D0

.text:0058EA43
.text:0058EA43 loc_58EA43: ; CODE XREF: sub_58EA10+15j
.text:0058EA43 pop ebx
.text:0058EA44 retn 4
.text:0058EA44 sub_58EA10 endp

Он передаёт значение 0 или 1 ещё двум функциям, каждая из которых принимает два параметра. Первый параметр — это значение переключения 0 или 1, а второй — всегда жёстко заданное значение 1. Посмотрев на эти две функции, можно увидеть, что они записывают в объект значение 0 или 1, а потом вызывают функцию

.text:005316B0 ; =============== S U B R O U T I N E =======================================
.text:005316B0
.text:005316B0
.text:005316B0 public sub_5316B0
.text:005316B0 sub_5316B0 proc near ; CODE XREF: sub_442070+1684p
.text:005316B0 ; sub_4C91E0+14Cp ...
.text:005316B0
.text:005316B0 arg_0 = byte ptr 4
.text:005316B0 arg_4 = dword ptr 8
.text:005316B0
.text:005316B0 mov edx, [esp+arg_4]
.text:005316B4 mov al, [esp+arg_0]
.text:005316B8 push edx
.text:005316B9 push 1
.text:005316BB mov [ecx+40Eh], al
.text:005316C1 call sub_5316F0
.text:005316C6 retn 8
.text:005316C6 sub_5316B0 endp
.text:005316C6
.text:005316C6 ; ---------------------------------------------------------------------------
.text:005316C9 align 10h
.text:005316D0
.text:005316D0 ; =============== S U B R O U T I N E =======================================
.text:005316D0
.text:005316D0
.text:005316D0 sub_5316D0 proc near ; CODE XREF: sub_442070+1698p
.text:005316D0 ; sub_4C91E0+137p ...
.text:005316D0
.text:005316D0 arg_0 = byte ptr 4
.text:005316D0 arg_4 = dword ptr 8
.text:005316D0
.text:005316D0 mov edx, [esp+arg_4]
.text:005316D4 mov al, [esp+arg_0]
.text:005316D8 push edx
.text:005316D9 push 1
.text:005316DB mov [ecx+40Fh], al
.text:005316E1 call sub_5316F0
.text:005316E6 retn 8
.text:005316E6 sub_5316D0 endp


Патчинг заменой
mov al, [esp+arg_0]
на
mov al, 0
nop
nop

приводит к тому, что мини-карта постоянно открыта, вне зависимости от состояния кнопки «Fog of War». Мы нашли код, отвечающий за раскрытие и сокрытие карты.

Разработка хака


На этом этапе можно создать хак, это будет всего лишь вопросом вызова 0x0058EA10 с значением true/false, в зависимости от нужного нам состояния карты. Однако тут есть небольшая проблема: существует команда записи в [ECX+0x53] по адресу 0x0058EA15. Это значит, что нам потребуется передать по адресу +0x53 объект с записываемым полем, который будет служить в качестве параметра "this", передаваемого обычно через ECX с помощью __thiscall. Далее в функции ECX перезаписывается после загрузки из постоянного адреса, поэтому это кажется безопасным подходом. Грязный код для этой задачи приведён ниже:

#include <Windows.h>
 
struct DummyObj
{
    char Junk[0x53];
};
DummyObj dummy = { 0 };
 
using pToggleMapFnc = void (__thiscall *)(void *pDummyObj, bool bHideAll);
 
int APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved)
{
    switch (dwReason)
    {
    case DLL_PROCESS_ATTACH:
    {
        (void)DisableThreadLibraryCalls(hModule);
 
        pToggleMapFnc ToggleMap = (pToggleMapFnc)0x0058EA10;
 
        while (!GetAsyncKeyState('0'))
        {
            if (GetAsyncKeyState('7'))
            {
                ToggleMap(&dummy, true);
            }
            else if (GetAsyncKeyState('8'))
            {
                ToggleMap(&dummy, false);
            }
 
            Sleep(10);
        }
 
        break;
    }
 
    case DLL_PROCESS_DETACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
        break;
    }
 
    return TRUE;
}

После инъекции DLL в процесс игры можно переключать карту в полностью открытый или закрытый вид с помощью клавиш «7» и «8».





Заключение


На этом мы завершаем разработку хака карты для игры. Этот подход был очень запутанным и сложным, и в следующей части статьи я покажу, как можно сильно упростить всё благодаря использованию полезной информации, оставленной разработчиками в исполняемом файле. Читая статью, можно решить, что работа с начала и до конца была довольно линейной, но на самом деле ради краткости пропущены многие пути в коде, которые привели меня в тупики. Если бы они остались, то они сами и их объяснение могли потянуть по объёму на диссертацию. При первоначальной разработке хака я множество раз прошёл по различным путям выполнения кода, делая заметки о том, что может оказаться необходимым. В результате в этой статье собрана только полезная информация, скомпонованная в целостное и почти линейное руководство.

Часть вторая: лёгкий способ


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

В этой части я буду использовать x64dbg, отличный отладчик и дизассемблер, который я считаю преемником устаревшего уже OllyDbg. К сожалению, в этой части я не часто использовал его, потому что мне почти не требовался анализ в процессе выполнения кода (в конце концов, эта часть называется «простой способ»). Фрагменты на ассемблере вставлены из IDA Pro, потому что я считаю её формат копирования-вставки наиболее читабельным.

Начав с подключения к процессу и выполнения дампа строк (правая клавиша мыши -> Search for -> Current Module -> String references) основного исполняемого файла, мы получили 25817 строк — довольно большая область для поиска.



Фильтр по строке «map» даёт нам более удобный набор. Просмотрев его, я нашёл несколько строк, которые могут привести к чему-то интересному:

«trSetFogAndBlackmap(<true/false> <true/false>): fog and black map on/off.»
«trRevealEntireMap — shows whole map, similar to how revealed mode works»
«trPlayerResetBlackMap(: Resets the black map for a given HUMAN player.»
«map visibility»
«blackmap([integerState]): toggles or sets unexplored black map rendering.»


Две самые многообещающие строки я выделил оранжевым. Строки дают чёткое представление о том, что делает функция, и даже сообщают аргументы параметров. Похоже, что функции «trX» связаны с имеющейся в игре системой триггеров, которая позволяет создателям карт добавлять эффекты и условия. Изучение ссылок в первой строке приводит к следующему:

...
.text:008B2B76 loc_8B2B76: ; CODE XREF: sub_8AE4A0+46CDj
.text:008B2B76 mov ecx, esi
.text:008B2B78 call sub_59C270
.text:008B2B7D push 1
.text:008B2B7F push offset loc_8AAEE0
.text:008B2B84 push offset aTrsetfogandbla ; "trSetFogAndBlackmap"

.text:008B2B89 mov ecx, esi
.text:008B2B8B call sub_59BE80

.text:008B2B90 test al, al
.text:008B2B92 jnz short loc_8B2BAE
.text:008B2B94 push offset aTrsetfogandbla ; "trSetFogAndBlackmap"
.text:008B2B99 push offset aSyscallConfigE ; "Syscall config error - Unable to add th"...
.text:008B2B9E push esi ; int
.text:008B2B9F call sub_59DBC0

...


Код здесь начинается с передачи строки, указателя на функцию и константы (1) как аргументов для другой функции (бирюзовый). Возвращаемое значение этого вызова проверяется на равенство 0, что является состоянием ошибки (синий). Взглянув на то, что происходит в дизассемблере, можно заметить, что этот шаблон используется везде. Этот код и окружающий его код пытается зарегистрировать триггеры и сообщает имя триггера, механизм обработки события к месту, где находится код триггера, и пока неизвестную константу 1. С учётом этого, стоит продолжить поиск в механизме обработки события.

Переход к механизму обработки события приводит нас к следующему фрагменту кода:

.text:008AAEE0 loc_8AAEE0: ; DATA XREF: sub_8AE4A0+46DFo
.text:008AAEE0 mov eax, dword_A9D244
.text:008AAEE5 mov ecx, [eax+140h]

.text:008AAEEB test ecx, ecx
.text:008AAEED jz short locret_8AAF13
.text:008AAEEF mov edx, [esp+4]
.text:008AAEF3 push 0
.text:008AAEF5 push edx
.text:008AAEF6 call sub_5316B0

.text:008AAEFB mov eax, [esp+8]
.text:008AAEFF mov ecx, dword_A9D244
.text:008AAF05 mov ecx, [ecx+140h]
.text:008AAF0B push 0
.text:008AAF0D push eax
.text:008AAF0E call sub_5316D0

.text:008AAF13
.text:008AAF13 locret_8AAF13: ; CODE XREF: .text:008AAEEDj
.text:008AAF13 retn


Два вызова здесь (зелёных) должны быть вам знакомы, если вы внимательно читали первую часть статьи. Это две функции, которые, как мы обнаружили, управляют раскрытием и сокрытием карты. Каждая функция получает указатель "this", который, как мы видим здесь, загружается с постоянного адреса и скорее всего является классом для главного игрока вмести со значением true/false, описывающим то, что происходит с картой. Здесь есть третий неизменяемый параметр 0, который отличается от неизменяемого параметра 1 в другом месте вызова из предыдущей части статьи. Возможно он указывает, что состояние карты изменено игроком или триггером.

Зная это, хак из предыдущей части можно сделать немного лучше. В старом хаке была проблема с предоставлением фальшивого указателя "this", который должен был иметь записываемое поле, и имелся только один вариант переключения: true/false. Исходя из документации, полученной дампом строк, эта функция принимает два булевых значения; предположительно, они управляют наложенным чёрным цветом и туманом войны, затеняющим области, уже исследованные игроком, но которые игрок в данный момент не видит.

Новый (но всё ещё немного грязный) представлен ниже:

#include <Windows.h>
 
using pToggleMapFnc = void (__cdecl *)(bool bEnableBlackOverlay, bool bEnableFogOfWar);
 
int APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved)
{
    switch (dwReason)
    {
    case DLL_PROCESS_ATTACH:
    {
        (void)DisableThreadLibraryCalls(hModule);
 
        pToggleMapFnc ToggleMap = (pToggleMapFnc)0x008AAEE0;
 
        while (!GetAsyncKeyState('0'))
        {
            if (GetAsyncKeyState('6'))
            {
                ToggleMap(true,  true);
            }
            else if (GetAsyncKeyState('7'))
            {
                ToggleMap(true, false);
            }
            else if (GetAsyncKeyState('8'))
            {
                ToggleMap(false, true);
            }
            else if (GetAsyncKeyState('9'))
            {
                ToggleMap(false, false);
            }
 
            Sleep(10);
        }
 
        break;
    }
 
    case DLL_PROCESS_DETACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
        break;
    }
 
    return TRUE;
}

Вызывая функцию с разными сочетаниями параметров, удалось получить следующее поведение:

True/True — наложенный чёрный цвет с туманом войны
True/False — нет наложенного чёрного, туман войны есть. Пометки на карте отсутствуют.
False/True — наложенный чёрный без тумана войны. Исследованные области всегда видимы.
False/False — нет наложенного чёрного цвета, нет тумана войны. Видима вся карта.

Ниже показаны скриншоты для всех четырёх состояний:









Хак стал более чистым, потому что теперь он выполняет прямой вызов функции и не требует передачи ничего неизвестного. Надеюсь, очевидно, почему я считаю это «лёгким способом», в отличие от предыдущего решения, потребовавшего долгой отладки и трассировки.

В следующей, последней части статьи мы подумаем, как сделать этот хак ещё немного чище и профессиональнее. Кроме того, мы рассмотрим, что необходимо для портирования хака под новую версию игры Extended Edition.

Часть третья: собираем всё вместе


В предыдущих двух частях мы говорили о том, как разработать хак карты Age of Mythology. Мы сделали это, найдя и выполнив обратную разработку частей игры, ответственных за переключение состояний карты (наложенный чёрный слой, туман войны, полностью раскрытая карта) и вызывая эти функции через DLL, инъектированную в процесс игры. В этой короткой части мы завершим тему, добавив в исходный код инъектор, который будет инъектировать разработанную нами DLL хака в процесс Age of Mythology. Хак будет работать в многопользовательском режиме, в оригинальной игре и в расширенной версии.

Код выложен на github и в целом не требует объяснений. DLL хака карты экспортирует обратный вызов KeyboardProc, который управляет логикой переключения состояний карты в зависимости от нажатых пользователем клавиш (7, 8, 9, 0). Инъектор устанавливает в процесс игры хук клавиатуры, который инъектирует DLL хака в процесс игры и активизирует обратный вызов KeyboardProc. После этого все передаваемые игре нажатия клавиш перехватываются и проверяются на соответствие четырём клавишам переключения состояний карты. Если нажата клавиша переключения, то вызывается соответствующая функция, изменяющая состояние карты.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+28
Comments14

Articles