Взламываем Age of Empires III, чтобы изменить настройки качества шейдеров

Автор оригинала: Lancelot de Ferrière
  • Перевод
Начало мая 2020 года — если вы похожи на меня, то карантин заставил вас перепройти заново игры, которые не запускали долгие годы.

А если вы ещё больше похожи на меня, то у вас где-то мог заваляться диск с Age of Empires 3. Возможно, вы играете на Mac, возможно, вы ещё не обновились до Catalina и желаете покомандовать Морганом Блеком.

Итак, вы запускаете игру, попадаете в главное меню, и сразу же замечаете — что-то не так… Меню выглядит отвратительно.


Если вам интересно что же именно «отвратительно», то обратите внимание на воду. Всё остальное тоже ужасно, но это менее очевидно.


Итак, вы заходите в опции, поднимаете все параметры до максимума… Но игра по-прежнему уродлива.

Вы замечаете, что опции "Shader Quality" подозрительно заблокированы на "Low".

Попытка №1 — хакаем параметры


На этом этапе вы начинаете искать папку игры, которая, предположительно, создана где-то в папке Documents, потому что игра 2005 года, а тогда все так делали.

Что же мы ищем? Разумеется, файл, в котором хранятся параметры. Интерфейс меню не позволяет нам менять настройки, но мы же хитры, не так ли?

Мы находим нужный XML, потому что игра 2005 года и это, разумеется, XML, где натыкаемся на опцию "optiongrfxshaderquality", которой присвоено значение 0. Похоже, её мы и искали, поэтому мы повышаем значение до 100, ведь много качества не бывает. Здесь я с вами соглашусь.


Вы также можете заметить, что это ужасное использование XML. К счастью, у него и не бывает хороших применений.

Довольные собой, мы запускаем игру. Увы, ничего не изменилось. Заглянув снова в файл XML, мы видим, что параметр снова имеет значение 0. Ensemble Studios (да покоится она с миром) с презрением смотрит на всю вашу изобретательность.

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

Но мы не будем сдаваться. Потому что мы помним, как должна выглядеть Age 3. Мы помним, как любовались этой красивой волнистой водой. Этими эффектами частиц. Этими огромными кораблями, которые просто не помещаются в кадр. А, это просто камера была приближена, о чём я только думал?

Попытка №1 — хакаем данные


В самом начале папки в Documents есть файл log. Игра записала туда графическую карту и сообщила, что выбрала настройку «generic dx7». В нём написано, что у вас установлена Intel GMA 950, которая действительно была на предыдущем компьютере.

Но на этом у вас Intel Iris Graphics 6100. Что-то и в самом деле не так. Вы делаете предположение — игра определяет графические возможности компьютера, сравнивая его с базой данных графических карт вместо проверки возможностей самой карты. Потому что именно так делают AAA-разработчики, и если ты работал над open-source RTS, то знаешь, как это происходит.


Содержимое файла log. Если вам кажется забавным, что Intel обозначен как 0x8086, то вы выбрали подходящую статью.

Случайно вы находите старые примечания к патчу, которые подтверждают ваши опасения. Вы заглядываете в папку GameData folder, но там только файлы .bar и .xmb, которые в основном нечитаемы. Ищете при помощи grep "Intel" и "dx7". Ничего не происходит. Удаляете несколько файлов… Ничего не происходит. Но вы знаете, что AAA-игры могут хранить некоторые ресурсы в странных местах, а это порт Windows-игры на Mac, так что здесь всё может быть очень причудливым.

В конечном итоге вы находите файл «render.bar», если его переместить, то игра вылетает при запуске. Пока не особо понятно, но уже неплохо. Мы открываем файл в Hex Fiend, потому что эта программа обычно позволяет узнать что-нибудь о двоичных файлах. Так и получается. Часть этих данных представляет собой читаемый текст. Часть из них — это шейдеры. Но большинство из данных странным образом разделено пустым пространством…


Интерфейс Hex Fiend. Он минималистичен, зато работает. Первые байты — это буквы «ESPN», которые скорее всего являются заголовком .bar.

Вы восклицаете: «Очевидно, это UTF-16!» Игра создавалась для Windows, так что совершенно логично, что она хранит текст в UTF-16, впустую тратя почти половину от 30-мегабайтного файла данных. Ведь Windows любила UTF-16 (хоть и не должна была), так почему бы не использовать эту кодировку и ASCII-шейдеры в одном файле. Вполне логичная идея!

(На самом деле, чтобы разобраться, мне потребовалось около дня)

В Hex Fiend есть опция парсинга байтов как UTF-16, поэтому мы снова ищем grep строку dx7. Находится несколько вхождений. Мы заменяем их на строку dx9, которая тоже встречается в данных, сохраняем файл и запускаем игру.

М-да. В логах снова не выводится «dx9». Вероятно, это задаётся где-то ещё.

Давайте попробуем что-нибудь другое. Предположим, что игра распознаёт графические карты, а логах сохраняются их шестнадцатеричные идентификаторы (в моём слуае, 0x8086 — это Intel, а ещё 0x2773). Ищем «8086» в Hex Fiend, что-то находим, и часть текста выглядит многообещающе. Intel 9 Series — это серия GMA 950, и на ней Age 3, вероятно, работала не очень хорошо.


Некоторые числа походят на идентификаторы карт (2582, 2592); возможно, если их заменить, то что-то изменится. Мы всё равно создали копию файла, так что попробовать можно.

Увы, но это тоже тупик. На самом деле, мы изучаем не тот файл. К счастью, я могу вам это сказать, потому что я потратил на это много времени и попробовал слишком многое. На эту чертовщину потребовалось около 20 часов. И это не считая времени на все скриншоты…

Нужный файл называется «DataP.bar», и если мы с умом заменим ID то в логах увидим нечто совершенно новое:


Не совсем понимаю, почему этот «generic dx7» не такой же, как на скриншоте выше.

Разумеется, в игре мы всё равно ограничены настройками «Low», то есть логи не врут. Мы надеялись на «Medium», но увы.

Похоже, большего от хакинга данных не добиться. Настало время доставать инструменты посерьёзнее.

Попытка №2 — взлом


Итак, если ничего не удалось, нам осталось последнее: изменить сам исполняемый файл (то есть, как говорили в 2005 году, «крякнуть» его). Кажется, сегодня это называется просто хакингом, но этот термин сохранился в сфере игрового пиратства (разумеется, я лично никогда таким не занимался).

Мы переходим к самой интересной части статьи (да, вы уже прочитали тысячу слов, но разве это не было любопытно?).

Дизассемблер Hopper


На этом этапе нашим инструментом будет дизассемблер: приложение, получающее двоичный код исполняемого файла и превращающее его в читаемый (ха-ха) ассемблерный код. Самым популярных из них является IDA Pro, но он безумно дорогой и я не уверен, что он работает на Mac, поэтому я воспользуюсь Hopper. Он не бесплатен (стоит 90 долларов), но его можно свободно использовать в течение 30 минут за раз с почти всеми необходимыми функциями.

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

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

И ещё одно примечание: «крэкинг» чаще всего незаконен и/или выполняется в незаконных целях. Я демонстрирую довольно редкий пример достаточно законного использования, который в то же время вполне «white hat», поэтому здесь всё в порядке. Предположу, что, строго говоря, это незаконно/противоречит соглашению конечного пользователя, но, очевидно, является случаем форс-мажора. Как бы то ни было, платите за то, что вам нравится, и только за это, потому что антипотребительство — это круто.




Перетащите приложение Age 3, чтобы открыть его. Только выберите «x86» вместо powerPC, потому что игра-то 2005 года.

Этап 1 — поиск нужного фрагмента


Как ни удивительно, но это может быть самым сложным. У нас есть дизассемблированный код, но мы понятия не имеем, где искать. Да, в некоторых играх есть символы отладки, благодаря чему имена функций можно прочесть, но не в нашем случае. Hopper даёт нам названия типа «sub_a8cbb6», с которыми нам придётся разбираться самостоятельно.

К счастью, у нас есть фора: наш файл лога. Высока вероятность того, что при записи в лог используются строковые литералы, то есть ASCII прошиты в исполняемом файле. Именно их мы и можем поискать с помощью grep, потому что от них не избавишься никаким компилированием (однако их можно скрыть обфускацией кода. К счастью, этого не произошло.). Поиск grep строковых литералов — обычно первое, что делают при дизассемблировании программы. Итак, давайте введём в поле поиска Hopper «found vendor», и он кое-что обнаружит:


О, да, строковые литералы, обожаю их.

Само по себе это нас не так сильно продвинуло вперёд. Но так как адрес этой «строки литералов» жёстко прописан, мы можем найти на него ссылки. Hopper выделил их справа зелёным: «DATA XREF=sub_1d5908+1252». Дважды щёлкнув на строку, мы перейдём к нашему герою — процедуре sub_1d5908.


Здесь мы находим ассемблерный код. Считается, что его сложно читать, но это не так. Самое тяжёлое — это понять его.

По умолчанию Hopper использует «синтаксис Intel», то есть первый операнд — это получатель, а второй — источник. В выделенной строке мы видим mov dword [esp+0x1DC8+var_1DC4], aXmlRenderConfi. Давайте разберём это.

Наша первая строка на ассемблере


mov — это команда MOV, известная тем, что является на x86 Тьюринг-полной. Она перемещает (на самом деле, копирует) данные из A в B, что по сути означает считывание A и запись её в B. С учётом вышесказанного, aXmlRenderConfi — это A, а dword [esp+0x1DC8+var_1DC4] — это B, получатель. Давайте разберём это подробнее.

aXmlRenderConfi — это значение, которым нам помогает Hopper. На самом деле, это псевдоним для адреса памяти строкового литерала, на который мы щёлкнули несколько секунд назад. Если разделить окно и посмотреть на код в режиме Hex (который является предпочтительным режимом для хакеров), то мы увидим там 88 18 83 00.


Hopper удобно подсвечивает одинаковые выбранные фрагменты в обоих окнах.

Если «перевернуть» значение правильно, то мы получим 0x00831888 — адрес памяти строкового литерала (на одном из показанных выше скриншотов от даже подсвечен жёлтым). Итак, одна загадка решена: код выполняет MOV, то есть записывает адрес 0x00831888.

Почему он записан как 88 18 83 00, а не как 00 83 18 88? Так получилось из-за порядка байтов — довольно запутанной темы, которая обычно оказывает небольшое влияние. Это одна из тех вещей, из-за которых люди говорят, что «компьютеры не работают». Для нас это значит то, что эта проблема будет встречаться у всех чисел, с которыми мы будем сегодня работать.

Вы могли заметить, что я сказал «записываем адрес», и это верно: нас на самом деле не волнует содержимое, которое оказалось строковым литералом с нулевым байтом в конце. Мы записываем адрес, потому что код позже будет ссылаться на этот адрес. Это указатель.

А что насчёт dword [esp+0x1DC8+var_1DC4]? Здесь всё сложнее. dword означает, что мы работаем с «двойными словами (double-words)», то есть с 16*2 битами. То есть мы копируем 32 бита. Напомним: наш адрес равен 0x00831888, то есть 8 шестнадцатеричным символам, а каждый шестнадцатеричный символ может иметь 16 значений, то есть 4 бита данных. Так мы получаем 8*4 = 32. Кстати, отсюда же взялось обозначение «32-битный» для компьютерных систем. Если бы мы использовали 64 бита, то адрес памяти записывался бы как 0x001122334455667788, был бы вдвое длиннее и в 2^32 раз больше, и нам бы пришлось копировать 16 байт. Так бы у нас было гораздо больше памяти, но и на копирование указателя требовалось бы вдвое больше работы, и поэтому 64 бита на самом деле не вдвое быстрее 32 бит. Кстати, более подробное объяснение термина «слово» можно найти здесь. Потому что он, разумеется, сложнее, чем моё объяснение.

Так, а что насчёт части в квадратных скобках? Скобки означают, что мы вычисляем то, что находится внутри, и считаем это указателем. Следовательно, команда MOV будет выполнять запись по адресу памяти, полученному в результате вычислений. Давайте снова разберём это подробнее (и не волнуйтесь, закончив с этим, вы, по сути, будете знать, как читать ассемблерный код с синтаксисом Intel).

esp это регистр, который в ассемблере является ближайшим эквивалентом переменной. В архитектуре x86 много регистров, и у разных регистров есть разные способы применения, но нас это особо не будет волновать. Подробнее об этом можно узнать здесь. Просто знайте, что существуют особые регистры для чисел с плавающей запятой, а также «флаги», которые используются для сравнений и подобных им операций. Всё остальное — это просто числа, записанные в шестнадцатеричной форме, которые Hopper немного переработал, чтобы помочь нам. var_1DC4 — это -0x1DC4, мы можем увидеть это в начале процедуры, или на правой панели. Это означает, что результатом вычисления [esp+0x1DC8+var_1DC4] будет [esp + 0x1DC8 — 0x1DC4], что равняется [esp + 4]. По сути, это означает «взять 32-битное значение регистра ESP, прибавить 4, и интерпретировать результат как адрес памяти».

Итак, повторим: мы записываем адрес строкового литерала в «esp+4». Это верная информация, но она совершенно бесполезна. Что это означает?

В этом-то и заключается сложность при чтении ассемблерного кода: в разборе того, что он должен делать. Здесь у нас есть фора: мы знаем, что он, скорее всего, что-то записывает в лог. Поэтому можно ожидать вызов функции, которая выполняет запись в лог. Давайте её поищем.


На скриншоте выше есть несколько похожих вызовов, а также команда call, вызывающая процедуру. Hopper обычно говорит об этом (не знаю, почему он не сделал этого здесь), но по сути, мы задаём аргументы для вызова функции. Если понажимать по разным вызовам подпрограмм, то найдём нечто с названием imp___jump_table___Z22StringVPrintfExWorkerAPcmmPS_PmmPKcS_. Это декодированное имя функции, и части «Printf» достаточно, чтобы понять, что она действительно что-то выводит. Нас не волнует, как она это делает.

Сделаем шаг назад


На этом этапе мы совершенно отошли от того, чем занимались: мы пытались заставить убедить игру, что наша графическая карта 2015 года достаточно мощна, чтобы запустить игру 2005 года. Давайте подведём итог. Мы нашли место, где выполняется запись лога. Если нам повезёт, то здесь же находится код, проверяющий параметры и возможности графических карт. К счастью, нам действительно повезло (есть большая вероятность, что в противном случае этой статьи бы не было).

Чтобы получить общую картину, мы воспользуемся функцией Control Flow Graph программы Hopper, которая разбивает код на небольшие блоки и рисует стрелки, обозначающие разные переходы. Так гораздо проще разобраться, чем в обычном ассемблере, в котором часто всё находится в беспорядке.


Вызов записи в лог находится посередине ужасно длинной функции, потому что это, опять-таки, обычно бывает в AAA-играх. Здесь нам стоит поискать другие строковые литералы и «подсказки» Hopper, надеясь что-нибудь найти. В начале процедуры находится оставшаяся часть логгинга, а ниже есть интересные части о «forcing dx7» и параметрах «Very High», а также о проблеме с версией пиксельных шейдеров… Которые у нас и есть.

Лог наших «хакнутых» данных сообщал, что пиксельные шейдеры у нас версии 0.0, и они несовместимы с параметрами «High». Этот код находится в левом нижнем углу нашей функции, и если присмотреться, можно увидеть нечто любопытное (выделенное вашим покорным слугой): это четырежды повторённый одинаковый код для параметров «Very High», «High», «Medium» и «Low». И да, чтобы найти это, мне понадобилось несколько часов.


Я выделил синим блок, в котором выводится в лог «pixel shader version 0.0 High». Похожие части я выделил другими красивыми цветами.

Единственные отличия заключаются в том, что мы записываем в разных местах «0x0», «0x1», «0x2» и «0x3». Это подозрительно похоже на файл параметров, который мы рассматривали выше, и параметр optiongrfxshaderquality (возможно, вы пока этого не поняли. Но так и есть, поверьте мне).

На этом этапе мы можем попробовать пойти разными путями, что я и сделал. Но я буду кратким — мы займёмся самым нужным: выясним почему заканчивается неудачей проверка пиксельных шейдеров. Давайте поищем ветвление. Блок вывода в лог линеен, значит, оно должно быть где-то выше.


Фанаты SEMVER, узрите более совершенный формат версий: числа с плавающей запятой.

И в самом деле, прямо над ним есть jb — команда перехода (вспомните о «goto»). Это "условный переход", и условием является "если меньше". Ассемблерные «если» работают через флаги регистров, о которых я вкратце говорил выше. Достаточно просто знать, что нам нужно посмотреть на команду выше: скорее всего, она задаёт флаг. Часто это команда cmp или test, но здесь использована ucomiss. Это варварство. Но она легко гуглится: ucomiss. Это команда сравнения чисел с плавающей запятой, и это объясняет, почему в коде есть xmm0: это регистр чисел с плавающей запятой. Это логично: логи сообщают, что наша версия пиксельных шейдеров равна «0.0», что является значением с плавающей точкой. Так что же тогда находится по адресу памяти 0x89034c? Шестнадцатеричные данные имеют вид 00 00 00 40, и это может сбить вас с толку. Но мы знаем, что стоит ожидать чисел с плавающей запятой, поэтому давайте так и интерпретировать данные: это означает 2.0. Что является вполне логичным значением с плавающей запятой для версии пиксельных шейдеров, которую Microsoft действительно использовала в своём High-Level Shading Language, на котором написаны шейдеры DirectX. И Age 3 — это DirectX-игра, несмотря на то, что в порте на Mac используется OpenGL. Также логично, что в 2005 году для включения параметра «High» требуются пиксельные шейдеры версии 2.0, так что мы идём в нужном направлении.

Как же нам это исправить? Эта команда сравнения выполняет сравнение со значением в xmm0, которое задаётся в строке выше: movss xmm0, dword [eax+0xA2A8]. MOVSS — это особая команда «move» для регистров с плавающей запятой, и мы записываем 32 бита из адреса, который является результатом вычисления eax+0xA2A8.

Предположительно, это значение равно не 2.0, а, скорее, 0.0. Давайте это исправим.

Занимаемся настоящим хакингом


Наконец-то мы готовы добраться до параметров High игры. Мы хотим пройти по «красному» пути и пропустить всю логику с «неверной версией пиксельных шейдеров». Это можно сделать, успешно пройдя сравнение, то есть обратив условие перехода, но у нас есть ещё один трюк: код составлен таким образом, что при удалении перехода мы пойдём «красным» путём. Давайте просто заменим переход nop — операцией, которая ничего не делает (невозможно просто удалять или добавлять данные в исполняемый файл, потому что произойдёт смещение, и всё поломается).

Я рекомендую выйти для этого из CFG, потому что в противном случае Hopper начинает сходить с ума. Если сделать всё правильно, то в окне Hex мы увидим красные строки.



Красным показаны внесённые нами изменения. На этом этапе, если вы заплатили за Hopper, то можно будет просто сохранить файл. Но я не платил, поэтому я открою исполняемый файл в Hex Fiend, найду нужную область (скопировав область из Hopper во вкладку Find внутри Hex Fiend) и изменю её вручную. Здесь будьте аккуратны, можно всё сломать, поэтому рекомендую скопировать куда-нибудь исходный исполняемый файл.

Сделав это, запустите игру, зайдите в параметры… И ура — мы можем выбрать «High». Но не можем выбрать «medium». И не можем использовать высокие параметры теней. Тем не менее, у нас прогресс!


Просто взгляните на эти великолепные отражения в воде!

Усовершенствования


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

Как мы помним, игра загружает значение по адресу eax+0xA2A8. Можно воспользоваться Hopper, чтобы выяснить, что же там изначально записано. Нам нужно найти команду, в которой 0xA2A8 используется в качестве операнда «получатель» (я выбрал 0xA2A8, потому что это достаточно конкретное значение, и мы не можем быть уверенными, что тот же регистр не используется где-то ещё). Здесь нам очень пригодится подсветка Hopper, потому что мы можем просто нажать на 0xA2A8 и поискать в CFG жёлтые части.


Наша победа находится чуть выше: как и ожидалось, мы записываем значение какого-то регистра с плавающей запятой. Признаюсь, что не особо понимаю, что происходит перед этим (моё наилучшее предположение: игра проверяет возможности сжатия текстур), но это не особо важно. Давайте напишем «2.0» и продолжим работу.

Для этого мы воспользуемся «Assemble instruction» приложения Hopper, а затем проделаем те же манипуляции в Hex Fiend.




Мы используем MOV вместо MOVSS, потому что записываем обычное значение, а не особое значение регистра с плавающей запятой. К тому же оно с прямым порядком байтов, то есть инвертированное.

Вы заметите, что происходит что-то странное: мы «съели» команду jb. Так получилось потому, что новая команда занимает больше байтов, чем предыдущая, а как я говорил, мы не можем добавлять или удалять данные из исполняемого файла, чтобы сохранить смещения. Поэтому у Hopper нет иного выхода, кроме как уничтожить команду jb. Это может стать проблемой, и мы способны её устранить, если переместить команду вверх (ведь нам больше не нужно записывать значение xmm3). Здесь это сработает, потому что мы в любом случае выполняем ветвление по нужному пути. Повезло.

Если запустить игру… То ничего особо не поменяется. Я предположил, что Intel 9 series недостаточно быстра, поэтому игра жалуется. Это не проблема, потому что на самом деле мы стремимся к «Very High».

Какой была самая мощная карта примерно в 2004 году? NVIDIA GeforceFX 6800. Давайте обманом убедим игру, что именно она у нас и установлена.

Вниз по кроличьей норе


Да, это совершенно новый раздел.

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

У нас есть одна подсказка: опции ведут себя странно есть Low/High, но нет Medium. Так что, возможно, существует другой код, обрабатывающий все эти «Very High», «High», «Medium» и «Low». И он на самом деле есть, в той же функции. Он намного раньше в CFG, и кажется интересным.


Мы можем предположить, что сырые данные хранятся в каком-то XML, потому что это весьма вероятно, ведь есть несколько других файлов данных, являющихся XML. А XML по сути является кучей указателей и атрибутов. Поэтому код здесь скорее всего проверяет какие-то XML-атрибуты (и в самом деле, ниже есть «expected one of ID, Very High …», что очень похоже на проверку правильности файла). Мы можем представить себе подобную XML-структуру, в которой булевы значения определяют качества шейдеров:


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

Очень интересная часть показана в левой части CFG (для вас она может быть справа, но на скриншоте показана слева): мы видим ту же var_CC, которая используется в коде, записывающем в лог ID устройства. То есть ebp+var_CC, вероятно, относится к ID нашего устройства, 0x2773. Странно то, что в первой «проверке» нет строкового литерала, но это баг Hopper: адрес 0x0089da8e содержит шестнадцатеричные данные 69006400, что в UTF-16 расшифровывается как «id» (на самом деле, вероятно, Hopper не смог понять этого из-за UTF16). А мы как раз ищем ID.

Знаете что? Давайте-ка запустим отладчик и проверим всё на самом деле.

Отладка игры


Откроем окно терминала и запустим lldb (просто введите lldb и нажмите на enter). Сначала нам нужно приказать LLDB следить Age 3, а затем мы можем запустить игру, вызвав run. Игра запускается, и ничего полезного мы не получаем.


Теперь нам нужно добавить точку останова: чтобы подтвердить наши предположения, мы хотим останавливать выполнение, когда доберёмся до показанного выше кода. У нас нет исходного кода, но это не важно: мы можем установить точку останова по адресу памяти. А адрес у нас есть в коде. Давайте добавим останов на вызове MOV, получающем «id», его адрес — 0x001d5e68. Для добавления точки останова достаточно ввести br set -a 001D5E68. После запуска игра останавливается и LLDB показывает дизассемблированный код (в синтаксисе AT&T, а не Intel, поэтому все операнды инвертированы, но мы видим, что это тот же код). Можно заметить, что LLDB в этом случае на самом деле умнее Hopper; он сообщает нам, что мы выполняем операции, связанные с XML и выводом printf.



Уже ощущаете себя хакером?

Чтобы двинуться дальше, давайте «сделаем шаг» (step instruction). Короткая команда для этого
ni. Повторив несколько раз, мы заметим, что вернулись к тому, с чего начали. Это цикл! И это совершенно логично: вероятно, мы итеративно обходим какой-то XML. На самом деле, Hopper показывал нам это — если проследить за CFG, то мы увидим (возможный) цикл.


Это означает: if (*(ebp+var_CC) == *(ebp+var_28)) { *(ebp+var_1D84)=edi }

Его условие выхода заключается в том, чтобы значение ebp+var_1D84 было ненулевым. И мы видим интересный параметр кода в выделенной мной var_CC.

Давайте добавим точку останова на этой cmp и разберёмся.



Слева: задаём точку останова на CMP и продолжаем выполнение. Справа следим за регистрами и памятью.

Мы смотрим на регистры с reg r, и значение памяти с mem read $ebp-0x28. eax и в самом деле содержит 0x2773, а значение в памяти теперь равно 0x00A0. Если несколько раз выполнить thread c, то мы увидим, что происходит итерация по различным ID в поисках совпадений. На каком-то этапе значения совпадут, произойдёт выход из цикла, и начнётся игра. Теперь мы знаем, как этим управлять.

Вспомним файл лога: он определил и устройство, и производителя. Вероятно, где-то есть похожий цикл для названий производителей. И в самом деле, он очень похож и находится в CFG прямо над нашим циклом. Этот цикл чуть проще, и мы поищем строку «vendor».

На этот раз сравнение выполняется с var_D0. Эта переменная и в самом деле используется в качестве ID производителя. Если мы поставим здесь точку останова и всё проверим, то увидим знакомый 0x8086 в eax, и в то же самое время происходит сравнение с 0x10DE. Давайте запишем его в eax и посмотрим, что случится.



Ого.

То есть 0x10DE — это NVIDIA. На самом деле, можно было бы понять это и при изучении файла данных, но так не очень интересно. Теперь вопрос заключается в следующем: какой идентификатор у GeforceFX 6800? Мы можем использовать LLDB и просто проверить каждый идентификатор, но на это потребуется время (их много, я пробовал подобрать нужный). Поэтому давайте на этот раз взглянем на render.bar.


Предположу, что это «XMB» — двоичное представление XML, используемое в Age 3

Этот файл довольно хаотичен. Но после меток Geforce FX 6800 мы видим несколько идентификаторов. Скорее всего, нам нужен один из них. Давайте попробуем 00F1.

Проще всего протестировать это — воспользоваться LLDB и задать нужные точки останова. Я пропущу это этап, но скажу, что 00F1 подошёл (к счастью).

На этом этапе нам нужно ответить на вопрос: «Как сделать это изменение постоянным?» Похоже, что проще будет изменить значения var_CC и var_D0 на 0X00F1 и 0X10DE. Для этого нам всего лишь нужно получить пространство для кода.

Простым решением будет замена одного из вызовов записи лога на NOP, например, вызова подсистемы. Это освободит нам несколько десятков байт, а это даже больше, чем нужно. Давайте выберем всё это и заменим на NOP. Затем нам просто нужно записать при помощи MOV информацию в переменные: ассемблерные mov dword [ebp+var_CC], 0xF1 и mov dword [ebp+var_D0], 0x10DE.



До и после

Давайте запустим игру. Наконец-то мы чего-то добились: можно выбирать из Low, Medium или High, а также включить параметры High для теней.


Но мы всё равно не можем включить Very High, и это печально. Что происходит?


А, понятно.

Как оказалось, Geforce FX 6800 должна поддерживать пиксельные шейдеры версии 3.0, а мы задали только версию 2.0. Сейчас нам это уже легко исправить: достаточно записать 3.0 с плавающей запятой в ebp+0xA2A8. Это значение 0x40400000.

Наконец-то игра перестала капризничать, и мы можем наслаждаться параметрами Very High.


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

И это конец нашего пути, спасибо за чтение.

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

Графика на Medium вполне приемлема, но страдает от нехватки теней. Насколько я могу судить, в Very High по большей мере добавляется совершенно другая LUT (хорошее объяснение этого термина можно найти в разделе «Color Grading» этой статьи), туман вдалеке и, возможно, даже совершенно другая модель освещения? Картинка сильно отличается, и это довольно странно.





Сверху вниз: Very-High, High (с параметрами теней High), Medium (просто со включенными тенями), Low (с отключенными тенями).

Другие полезные ссылки


Блог, на который я давал ссылку выше, содержит замечательный анализ современных конвейеров рендеринга: http://www.adriancourreges.com/blog/

Оцените 0 A.D. — open-source RTS-игру того же жанра: https://play0ad.com. Ей нужны разработчики.

Если вы хотите больше узнать о формате XMB, то можете прочитать его код в 0 A.D.. Я не проверял, но почти уверен, что это тот же формат, потому что человек, написавший декодер XMB для Age 3 на Age of Empires heaven также реализовал в 2004 году эти файлы в исходном коде 0 A.D. Так что это будет забавным уроком палеонтологии кода. Спасибо тебе за всю работу, Ykkrosh.

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

В конце я хотел бы упомянуть проект Cosmic Frontier: ремейк классической серии Escape Velocity (больше Override, но он совместим и с Nova). Это потрясающая игра, и очень печально, что её мало кто знает. Сейчас создатели открыли кампанию на Kickstarter, а ведущий разработчик ведёт очень интересный dev blog, в котором рассказывает об использовании Hex Fiend для реверс-инжиниринга форматов данных оригинальной игры. Это отличное чтение. Если вы считаете, что моя статья была безумной. то в одном из постов они:

  • Запустили эмулятор классического Mac, чтобы использовать давно устаревшие API для считывания зашифрованного файла данных.
  • Выполнили реверс-инжиниринг шифрования из кода игры
  • Использовали хак файловой системы, чтобы получить доступ к этим же данным на современном Mac.

Да, это по-настоящему увлечённые люди, и я надеюсь, что их проект успешно завершится, потому что EV Nova — моя самая любимая игра.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

Комментарии 13

    +1
    Ведь Windows любила UTF-16 (хоть и не должна была)
    Всё же стоит учитывать, что когда Windows NT была в разработке, UTF-8 ещё не существовало, поэтому и был выбран UTF-16, потому что на тот момент он казался лучшим стандартом будущего.
      0
      Если бы. Был рассчёт на не более, чем 65536 символов и UCS-2. А вот когда в 1996м стало понятно, что их не хватит… был выбран неверный подход с переходом на UTF-16, вместо UTF-8.

      Ошибка была исправлена только через четверть века… но последствия мы будем расхлёбывать ещё очень долго.

      P.S. Если бы во время разработки Windows NT уже существовали бы и UTF-8 и UTF-16, то, почти наверняка, выбрали бы UTF-8, так как UTF-16 — это восхитительное собрание всех мыслимых и немыслимых проблем без каких-либо преимуществ.
        +1
        Разработка Windows NT началась в 1988 (когда она планировалась ещё как новая OS/2). UTF-8 появился в январе 1993. К этому времени Windows NT уже была почти готова (релиз в июле 1993), и ядро было полностью завязано на «широкие» юникодные строки с 16-битными кодпоинтами. Когда в UTF-16 появились суррогатные пары и вот это всё — дороги назад уже не было совсем. Microsoft нужно было завоёвывать рынок, и лишних 5 лет на переписывание внутренностей ещё совсем недавно созданной ОС не было. Тем более, что преимущества от переписывания были не очевидны: модные на тот момент технологии (вспомним Java и появившийся JavaScript) поддерживали Unicode именно в виде UTF-16, и скорее всего это всё ещё казалось светлым будущим.

        То, что в последних Windows 10 для Win32-приложений можно выбрать UTF-8 в качестве однобайтовой кодировки не отменяет, что внутри ОС всё в UTF-16 всё равно. UTF-8 просто перегоняется в UTF-16 и обратно тем же образом, как раньше поддерживались легаси-кодировки.

        P.S. Я знаю, что если придираться, то UCS-2 и UTF-16 — это разные вещи. Просто код, который работает с UTF-16, часто обращается с ним как с UCS-2 (программисты ленивы, или даже не в курсе про суррогатные пары, и в большинстве случаев это работает как ожидается); бывает и наоборот (имена файлов в NTFS в UCS-2 и могут быть невалидным UTF-16, но ОС пытается отображать эти имена именно как UTF-16). Такова реальность. Поэтому я воспринимаю UCS-2 и UTF-16 как нечто целое, где, так уж повелось, все по-разному обращаются с суррогатными парами.
          0
          Когда в UTF-16 появились суррогатные пары и вот это всё — дороги назад уже не было совсем.
          Дорога назад, конечно, была и она была столь же тривиальной, как и сегодня. Нужно было cp65001 завести не в XXI веке, а в 1995м году. И, соотвественно, Office97 (первый, где есть нормальная поддержка unicode) делать не на основе UCS-2, а на основе UTF-8.

          Но… вышло, как вышло.

          Microsoft нужно было завоёвывать рынок, и лишних 5 лет на переписывание внутренностей ещё совсем недавно созданной ОС не было.
          Не нужно было никаких «переписываний внутренностей». В те годы все операционки всё ещё работали со всякими ISO 2022. А софта, завязанного на UCS-2 или UTF-16 было нуль, нисколько.

          UTF-8 просто перегоняется в UTF-16 и обратно тем же образом, как раньше поддерживались легаси-кодировки.
          А это уже как раз — личное дело Windows, что там у неё внутри. Пока приложения могут считать что ничего, кроме UTF-8, в мире нету — пусть «внутри» хоть троичную систему счисления используют.

          Поэтому я воспринимаю UCS-2 и UTF-16 как нечто целое, где, так уж повелось, все по-разному обращаются с суррогатными парами.
          Собственно именно это и побуждает меня UTF-16 воспринимать только и исключительно как «ошибку истории». Потому что слишком много кода воспринимают UTF-16 как UCS-2, ломаются, тем или иным способом, на суррогатах — и слишком поздно это выясняется.

          Потому наилучший способ исправления программ с UTF-16 — прекратить её использовать. В UTF-8 слишком большой процент символов требуют многобайтового представления, «забыть» при этом и не получить гневных жалоб — практически невозможно. А ошибки в поддержке UTF-16 могут не правиться годами (не знаю, когда Firefox решил проблему с удалением «половины символа» при использовании Backspace, но это точно заняло не один год).
            +1
            Отдельная прелесть UTF-8 — возможность работать с строками как с обычными однобайтными, если у нас нет необходимости знать содержимое и считать «реальные» символы. Совместимость с ASCII очень сильно упрощает взаимодействие.
            Учитывая эту совместимость нет проблем переводить софт под виндой на UTF-8, в хулшем случае будут кракозябры, но ничего не сломается.
              0
              Учитывая эту совместимость нет проблем переводить софт под виндой на UTF-8, в хулшем случае будут кракозябры, но ничего не сломается.
              К сожалению всё не так просто. Вы можете вшить manifest в программу — и она будет даже использовать UTF-8 независимо от настроек системы… но вот только тот факт, что для этого требуется, чтобы ваши пользователи использовали «Windows Version 1903 (May 2019 Update)»… он так, чуть-чуть напрягает.

              Не все пользователи ещё перешли, однако.
      +2
      в Win версии все проще конечно. играл в Age of Mythology на Intel видео карте современной, определялась конечно же как старая и не давала поставить даже 32бита цвет, молчу про остальные графические настройки. Вылечил заменой текущей VendorID на самую крутую от nVidia доступную в конфиг файле. такой же файл есть и в win версии age of empires 3 называется configy.xml там содержится vendorid id и cardid id, заменяем их под самую топовую тогда GeForceFX 6800 и получаем на встроенной интел все доступные настройки
        +1
        Не знаю, расстроит вас это или обрадует, но скоро выйдет официальный ремастер :)
          0

          Very High на скриншотах не только выглядит иначе по цветам (мне не нравится). Там еще кусты пропали по пальмами. На остальных скриншотах они есть, даже в самом простом качестве.
          P.S. в игру не играл, может там, конечно пришел NPC и скосил траву, на я сомневаюсь :)

            0
            Этот файл довольно хаотичен. Но после меток Geforce FX 6800 мы видим несколько идентификаторов. Скорее всего, нам нужен один из них. Давайте попробуем 00F1.
            Проще всего протестировать это — воспользоваться LLDB и задать нужные точки останова. Я пропущу это этап, но скажу, что 00F1 подошёл (к счастью).
            На этом этапе нам нужно ответить на вопрос: «Как сделать это изменение постоянным?» Похоже, что проще будет изменить значения var_CC и var_D0 на 0X00F1 и 0X10DE. Для этого нам всего лишь нужно получить пространство для кода.

            Интересно, автор пробовал менять XMB файл вместо кода. По идее если 00F1 заменить на индификатор своей карты, то таким образом игра должна думать что у нас Geforce FX 6800… по идеи

              0
              Есть вероятность, что раз это некий двоичный формат, то там где-то есть чек-сумма с, возможно, неизвестным алгоритмом.
              0

              Что карантин с людьми делает :)
              На самом деле довольно интересно было почитать.

                0
                Интересная статья! интересно почитать

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

                Самое читаемое