Всем привет, В данной статье я расскажу о реверс-инжиниринге ресурсов игры Twisted Metal 4 для первой Playstation. В качестве основного инструмента я буду использовать Ghidra. Наверняка вы слышали об игровой серии Twisted Metal. А кому-то, наверное, довелось и поиграть (мне нет). По словам тех, кто играл в четвёртую часть, в игре имеются некоторые неприятные баги. Так вот, реверс-инжиниринг должен помочь исправить их. Поехали... Как обычно, разбор игры для первой плойки начинается с анализа основного исполняемого файла, в данном случае — SCUS_945.60. После непродолжительного осмотра данного файла, я пришёл к выводу, что в нём, фактически, большей части кода-то и нет. Значит, всё упрятано куда-то ещё. На диске, кроме основного исполняемого файла, обнаружились следующие форматы: IMG, MR, STR, TIM. Два последних являются стандартными для PSX, и представляют из себя видео-стримы и изображения соответственно. А вот на первые два надо бы взглянуть. Открываем в hex-редакторе первый попавшийся файл и видим:  Судя по "плотности" байт, файл чем-то упакован. Значит, нужно понять, где и как происходит распаковка. Для работы с исполняемыми файлами Sony Playstation 1 в Ghidra имеется загрузчик, я писал о нём здесь. Устанавливаем его, открываем Гидру, создаём новый проект и закидываем в него файл SCUS_945.60 из корня диска с игрой.  Узнаём, что при разработке игры использовался PsyQ 4.5.  Дожидаемся завершения анализа файла, и переходим в автоматически определившуюся функцию main().   Я выделил на скриншоте функцию, которая бросается в глаза — похоже, она имеет какое-то отношение к разбору MR-файлов. Посмотрим, что она делает.  В вызове этой функции в main() мы видели, что первым аргументом туда приходит имя файла, поэтому меняем тип первого аргумента на соответствующий — char * (у Гидры понятий const, не const нет), и переименовываем в mr_name.  Далее видим, что имя файла передаётся в следующую функцию: Зайдём в неё:  Здесь так же переименовываем и меняем тип аргумента. Первой же строчкой в этой функции видим использование какой-то переменной. Взглянув на неё, понимаем, что это константная строка.  Чтобы заставить Гидру определить эту строку как константу, нужно сначала обозначить её как строку (T->string).   Отлично, теперь мы видим, что это форматная строка, и она передаётся в какую-то функцию вторым аргументом. А это значит, что данная функция, скорее всего, sprintf(). Так её и переименуем. Далее идёт вызов другой функции, в которую передаётся результат вызова sprintf(). Заходим в функцию, и видим, что она каждый символ переданной строки делает прописным, вызывая toupper().  Переименовываем функцию в string_uppercase(). Следующая функция — FUN_80022210. Посмотрим, что она делает.  Судя по её второму аргументу — CdlLOC*, определяет местоположение файла на диске. Так её и назовём — get_file_location(). Теперь, чтобы типы аргументов функции get_file_location() применились в тех местах, где она функция вызывается, необходимо нажать клавишу P (Commit Params/Return) в C++-листинге.  Возвращаемся на уровень выше:  Приходим к выводу, что функция FUN_80021094 всего лишь определяет, где располагается файл на диске. Переименовываем в get_file_offset(), применяем типы аргументов (клавиша P) и возвращаемся к функции выше — к FUN_800217e8. Теперь данная функция выглядит следующим образом.  Видим вызов функции FUN_80021690, в которую передаются "координаты" файла, его имя, и второй аргумент функции. Зайдём и посмотрим.  Тут, вроде как всё просто и интуитивно, если учитывать, что мы имеем дело с координатами файла, его позицией и т.п. Делаю предположение, что первая вызываемая функция — FUN_80017cf8 — выделяет память под содержимое файла, которое будет прочитано далее. А вторая — собственно чтение. Для проверки я заглянул в первую функцию, и ужаснулся — значит, это точно выделение памяти! Посмотрим на вторую.  Да, очень похоже на чтение с диска. Переименовываем её в read_data_by_pos(). Поднимаемся выше, и переименовываем функцию FUN_80021690 в read_file_with_alloc(). Не забываем нажимать P для применения параметров.  Возвращаясь снова к этой функции, можно смело назвать её read_file(), и определить второй аргумент функции, как offset, с которого начинается чтение файла, ибо указатель на выделенную память занят за результатом функции, который она возвращает через return. И вот, мы снова вернулись в главную функцию, имея указатель на прочитанные из файла MAIN.MR данные.  Видим, что функция FUN_80012bf0 принимает этот указатель вторым аргументом. А первым снова идёт какая-то переменная (или константа). Уже имея в этом опыт, выясняем, что это — строка "/". Давайте теперь посмотрим, что же делает FUN_80012bf0.  Ясности первый осмотр функции, в то что она делает, не внёс. Зайдём в FUN_800128cc, которая, судя по листингу, не принимает аргументов и разберём её.  Аргументы у этой функции всё таки есть. Нажмём P. Видим что имеется вызов strcpy, но, почему-то с нераспознанными аргументами, исправим:  Зайдём в первую вызываемую функцию, куда передаётся первый аргумент, имеющий теперь тип char *:  Снова библиотечная функция, но на этот раз — strcmp(), добавим ей аргументы.  Становится понятно, что мы имеем дело со списком строк (DAT_8005963c), и их 16 штук (судя по условиям цикла). Исправим тип для указанного адреса на char*[16]. Для этого сначала перейдём по адресу 0x8005963C, зададим тип char* для первого указателя (через T), а затем нажмём клавишу [, чтобы создать массив.  Теперь функцию FUN_80012a24 можно переименовывать в find_prev_string_index(). Возвращаемся назад. Теперь мы видим довольно интересный кусок кода. Опытный глаз сразу определяет подсчёт длины strlen(), и выделение памяти под новую строку (ещё один malloc()?).  Но давайте проверим. Заходим в первую вызываемую функцию — FUN_8003d7a8:  Тут так и написано: "Possible STRLEN.OBJ/strlen". Почему же анализатор автоматически не применил сигнатуру, спросите вы? Варианта может быть два: Ну а вторая функция (FUN_80017cd8) оказалось такой же объёмной внутри, как и ранее найденный malloc(), поэтому смело называем данную функцию malloc2(). Итого, вся функция FUN_800128cc — это что-то типа strdup() с кэшированием. Переименовываем, и возвращаемся наверх.  Теперь начинается что-то интересное. Смотрим первый вызов:  Выглядит объёмно, но пока не очень сложно. Замечаем, что вызов strncpy() имеет больше аргументов, чем нужно. Но, почему-то в этой версии Ghidra (9.2) переопределить параметры мне не удалось. Видим ещё один вызов функции: Заглянем и туда:  Видим снова вызов strcmp() без параметров, исправляем (почему-то старое переопределение здесь не подтянулось). Отлично, у нас вырисовывается первое использование каких-то структур данных (видим использование смещений относительно указателей):  У Ghidra на эту тему есть готовая фича: Auto Create Structure (Shift+[). Жмём на таком указателе указанную комбинацию и получаем автоматически созданную структуру, которую останется только переименовать. Таких указателей у нас два: param_1 и iVar2.  Здесь мы имеем дело с односвязным списком: в field_0x8 первой структуры хранится указатель на список, в field_0xc второй структуры — следующий его элемент. Данные элемента хранятся в поле field_0x10. Переименуем всё соответствующим образом. Пока не понятно, это две разных структуры, или одна, но мы всегда сможем всё объединить. Вспоминаем, что мы зашли в эту функцию, чтобы понять что она делает, и понимаем, что здесь происходит поиск строки из аргумента param_2 в списке из аргумента param_1. Переименуем функцию в get_item_by_string(). Поднимаемся в место вызова этой функции, и видим следующую картину:  Вспоминаем, что get_item_by_string() возвращает указатель на элемент списка, сопоставляем с фактом, что результат функции записывается в param_1, и приходим к выводу, что те две структуры на самом деле являлись одной. Правим astruct_1 следующим образом:  Теперь можно разобраться вот с этим куском кода:  Судя по всему, в этих двух указателях хранятся два разных списка типа astruct_1, поэтому поменяем их типы на правильные и переменуем в str_list1 и str_list2. Остаётся эта непонятная куча кода со стековыми указателями, но с ней мне разобраться не удалось, т.к. у Гидры в целом плохо со строковыми переменными на стеке. Всё равно это не мешает понять, что делает функция FUN_80012148 — она ищет путь к файлу в списке и, если таковой имеется, возвращает указатель на элемент списка, иначе — NULL. Переименуем в get_item_by_full_path(). Снова поднимаемся наверх, и смотрим, что теперь представляет из себя функция FUN_80012bf0:  Судя по всему, мы добрались до разборщика MR-формата, т.к. у нас читается первый short (mr_mem пока имеет тип short*), а дальше в функцию FUN_80011f0c передаётся на идущие следом данные: Самое время создать для mr_mem новую структуру (через Auto Create Structure) и перейти к разбору FUN_80011f0c.  Я немного переименовал входные аргументы, чтобы было понятнее, остаётся разобраться, что здесь делается. Во первых, здесь осуществляется проход по структуре mr_sub_data (это та, которая начинается со смещения +0x04 — не +0x02 потому что short* — от начала mr_mem, т.е. от начала MR-файла). Во вторых, да, mr_sub_data это структура, поэтому давайте её создадим. Почему-то, в этот раз Ghidra с автоматическим созданием не справилась, создав слишком короткую структуру, поэтому сделаем это вручную (просто добавляем по байтовому полю, пока не пропадут обращения к указателю со структурой как к массиву):   Обратив внимание, что в текущей функции используется рекурсия при вызове FUN_80011f0c, решаем разобраться сразу с тем блоком кода, в котором она имеется, а именно:  В блоке с рекурсией есть всего одна неизвестная функция, поэтому мы сразу перешли в неё.  Выглядит достаточно просто: имеется множество обращений к полям структуры astruct_1, (которая, напоминаю, является списком), а также очевидно формирование новой подструктуры того же типа. Изменим типы полей и переименуем их так, чтобы итоговый код функции смотрелся приятнее, и получим следующее:  Теперь нас ждёт второй блок кода, в котором рекурсии не было. Переходим в единственную вызываемую из него функцию. Код оказался слишком большим, поэтому пришлось спрятать его под спойлер: Думаю, на этом моменте можно остановиться, т.к. код здесь довольно объёмный, а дальше будет только больше. Во второй (и, возможно, третьей) части у нас будет: На этом всё. Реверс-инженер