Мно­го лет назад я пытал­ся узнать, как работа­ет игра «Мор. Уто­пия» на самом деле. Но тог­да фор­мат ее скрип­тов ока­зал­ся мне не по зубам. Сегод­ня мы вскро­ем дви­жок игры, что­бы узнать, как работа­ют скрип­ты. И даже напишем собс­твен­ный деком­пилятор для неиз­вес­тно­го язы­ка прог­рамми­рова­ния!

info

Ес­ли тебе тоже инте­рес­но соз­дание модифи­каций для «Мора» — пред­лагаю объ­еди­нить наши уси­лия. До­бав­ляй­ся в чат по ревер­су игры!

 

Сбор информации

Скрип­ты хра­нят­ся в фай­ле Scripts.vfs. Фор­мат самодель­ный и прос­тей­ший, можешь почитать, как их вскры­вают, в одной из моих ста­рых ста­тей, где я показы­вал, как вскрыть и изме­нить ресур­сы «Ядер­ного тит­бита». Я решил не тра­тить на это вре­мя и рас­паковал архив готовым соф­том VFS Explorer.

Внут­ри лежит куча бинар­ных фай­лов с рас­ширени­ем .bin. Пер­вые четыре бай­та не содер­жат никакой замет­ной сиг­натуры.

Обыч­но раз­работ­чики встра­ивают про­верен­ные скрип­товые язы­ки вро­де Lua или Python. Раз файл бинар­ный, зна­чит, скрипт был ском­пилиро­ван. Одна­ко по раз­режен­ному (пол­ному нулей) байт‑коду ста­ло ясно, что фор­мат скрип­тов проп­риетар­ный. Толь­ко самодель­ные язы­ки так рас­точитель­но исполь­зуют прос­транс­тво фай­ла — срав­ни с опти­мизи­рован­ными про­цес­сорны­ми опко­дами. Нули в байт‑коде могут озна­чать, что коман­да занима­ет фик­сирован­ное количес­тво бай­тов, а аргу­мен­тов к ней не завез­ли.

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

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

 

Ищем точку входа

Что­бы узнать, какие поля есть в зак­рытом фор­мате на самом деле, поищем мес­то в коде игры, где чита­ются скрип­ты. Для это­го запус­тим игру в x64dbg, сов­ремен­ной аль­тер­нативе OllyDbg. Отладчик авто­мати­чес­ки под­гру­жает с дис­ка сим­волы, рас­став­ляя адре­сам в памяти ори­гиналь­ные наз­вания.

Но сна­чала отклю­чим пол­ноэк­ранный режим, для это­го изме­ним config.ini:

[Video]
XRes = 1024
YRes = 768
Fullscreen = 0

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

Текст журнала: OPEN: {ansi@[ESP+4]}
Условие остановки = 0
Условие добавления в журнал = 1

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

За­пус­каем игру и видим в жур­нале полот­но из игро­вых ассе­тов:

OPEN: C:\games\pathologic\alpha\data\scripts\fire.bin

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

До­пус­тим, я хочу оста­новить отладчик на кон­крет­ном фай­ле. Для это­го слег­ка изме­ним точ­ку оста­нова:

Условие остановки = strstr(ansi([ESP+4]), "fire.bin")

Фун­кция strstr воз­вра­щает еди­ницу, если в стро­ке, которую вер­нет ansi, будет подс­тро­ка fire.bin. Переза­пус­тив отладчик, ловим оста­нов­ку на нуж­ном фай­ле. Смот­рим в сте­ке адрес, отку­да при­шел вызов:

возврат к engine.CScriptManager::RunScript+88 из ???

По­хоже на точ­ку запус­ка скрип­та! Самое вре­мя открыть Engine.dll в IDA Pro.

 

Исследуем заголовки

IDA показы­вает не так мно­го вхо­дящих ссы­лок на CreateFileA, быс­тро находим фраг­мент кода внут­ри RunScript, чита­ющий файл скрип­та через вир­туаль­ную фай­ловую сис­тему (VFS).

v8 = __v.second.m_pManager->m_pFS->CreateMappedLoadObject(__v.second.m_pManager->m_pFS, pszScriptName);
// (...)
ScriptDataPtr = Script_2->GetMemoryPointer(Script_2);
CScript::CScript(ScriptDataPtr, __v.second.__vftable, v43)

Те­ло скрип­та переда­ется в конс­трук­тор объ­екта CScript. IDA весь­ма огра­ничен­но уме­ет работать с прог­рамма­ми, написан­ными на C++, поэто­му CScript::CScript — это прос­то имя фун­кции, никак не свя­зан­ное с клас­сом в целом. Мно­гие клас­сы име­ют таб­лицу вир­туаль­ных фун­кций, по которой мож­но вос­ста­новить их при­над­лежность, даже если у нас под рукой нет сим­волов.

void __userpurge CScript::CScript(CScript *this@<ecx>, CScript *pScript, unsigned int ulSize);

Сог­ласно про­тоти­пу фун­кции, пер­вый аргу­мент — это файл скрип­та и он же явля­ется телом клас­са. Такое решение час­то встре­чает­ся в ста­рых играх. Они эко­номи­ли на сери­али­зации и прос­то дам­пили тела клас­сов на диск, нап­ример для соз­дания сох­ранений. Пос­мотрим на опре­деле­ние клас­са CScript:

struct __cppobj __unaligned __declspec(align(4)) CScript
{
unsigned int m_ulGlobalVarCount;
boost::scoped_array<unsigned char> m_pGlobalVarTypes;
_STL::map<CEString,unsigned long,_STL::less<CEString>,_STL::allocator<_STL::pair<CEString const ,unsigned long> > > m_Properties;
unsigned int m_ulDataPoolSize;
boost::scoped_array<char> m_pDataPool;
unsigned int m_ulGlobalCount;
boost::scoped_array<CScript::GLOBAL_FUNCTION> m_pGlobals;
unsigned int m_ulTaskCount;
boost::scoped_array<CScript::TASK> m_pTasks;
_STL::map<unsigned long,CScript::EVENT,_STL::less<unsigned long>,_STL::allocator<_STL::pair<unsigned long const ,CScript::EVENT> > > m_GlobalEvents;
unsigned int m_ulCodeSize;
boost::scoped_array<boost::scoped_ptr<IInstruction> > m_pCode;
unsigned int m_ulRunTask;
unsigned int m_ulRunOp;
_STL::set<IScriptNotify *,_STL::less<IScriptNotify *>,_STL::allocator<IScriptNotify *> > m_Notify[3];
};

Струк­тура про­чита­на из PDB-фай­ла. IDA не уме­ет нор­маль­но работать с шаб­лонами, поэто­му эти страш­ные длин­ные строч­ки с кучей запятых и угло­вых ско­бок — не более чем наз­вания сиш­ных струк­тур. В гла­за бро­сает­ся схо­жесть тела клас­са со струк­турой скрип­та, опи­сан­ной на фанат­ском сай­те. С ней мож­но работать.

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии