Всем привет,
В данной статье я расскажу о реверс-инжиниринге ресурсов игры 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
.
Далее видим, что имя файла передаётся в следующую функцию:
iVar1 = FUN_80021094(mr_name,&local_8);
Зайдём в неё:
Здесь так же переименовываем и меняем тип аргумента. Первой же строчкой в этой функции видим использование какой-то переменной.
FUN_80034644(auStack128,&DAT_80057b84,mr_name);
Взглянув на неё, понимаем, что это константная строка.
Чтобы заставить Гидру определить эту строку как константу, нужно сначала обозначить её как строку (T
->string
).
Отлично, теперь мы видим, что это форматная строка, и она передаётся в какую-то функцию вторым аргументом. А это значит, что данная функция, скорее всего, sprintf()
. Так её и переименуем.
sprintf(auStack128,"%s;1",mr_name);
Далее идёт вызов другой функции, в которую передаётся результат вызова sprintf()
.
FUN_800102c0(auStack128,auStack128);
Заходим в функцию, и видим, что она каждый символ переданной строки делаем прописным, вызывая toupper()
.
Переименовываем функцию в string_uppercase()
.
Следующая функция — FUN_80022210
. Посмотрим, что она делает.
Судя по её второму аргументу — CdlLOC*
, определяет местоположение файла на диске. Так её и назовём — get_file_location()
.
undefined4 get_file_location(char *param_1,CdlLOC *param_2)
Тип
CdlLOC
определён в PsyQ и хранит часы, минуты, секунды позиции файла на диске.
Теперь, чтобы типы аргументов функции 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 *
:
iVar1 = FUN_80012a24(param_1);
Снова библиотечная функция, но на этот раз — strcmp()
, добавим ей аргументы.
Становится понятно, что мы имеем дело со списком строк (DAT_8005963c
), и их 16 штук (судя по условиям цикла). Исправим тип для указанного адреса на char*[16]
. Для этого сначала перейдём по адресу 0x8005963C
, зададим тип char*
для первого указателя (через T
), а затем нажмём клавишу [
, чтобы создать массив.
Теперь функцию FUN_80012a24
можно переименовывать в find_prev_string_index()
. Возвращаемся назад.
Теперь мы видим довольно интересный кусок кода. Опытный глаз сразу определяет подсчёт длины strlen()
, и выделение памяти под новую строку (ещё один malloc()
?).
Но давайте проверим. Заходим в первую вызываемую функцию — FUN_8003d7a8
:
Тут так и написано: “Possible STRLEN.OBJ/strlen“. Почему же анализатор автоматически не применил сигнатуру, спросите вы? Варианта может быть два:
3.0
)Ну а вторая функция (FUN_80017cd8
) оказалось такой же объёмной внутри, как и ранее найденный malloc()
, поэтому смело называем данную функцию malloc2()
.
Итого, вся функция FUN_800128cc
— это что-то типа strdup()
с кэшированием. Переименовываем, и возвращаемся наверх.
Теперь начинается что-то интересное. Смотрим первый вызов:
iVar2 = FUN_80012148(DAT_80056f38,rel_path);
Выглядит объёмно, но пока не очень сложно. Замечаем, что вызов strncpy()
имеет больше аргументов, чем нужно. Но, почему-то в этой версии Ghidra (9.2
) переопределить параметры мне не удалось.
Видим ещё один вызов функции:
param_1 = FUN_800120d8(param_1,pcVar5);
Заглянем и туда:
Видим снова вызов 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
передаётся на идущие следом данные:
if (*mr_mem == 0x67) {
FUN_80011f0c(paVar2,mr_mem + 2,iVar1);
uVar3 = 1;
}
Самое время создать для mr_mem
новую структуру (через Auto Create Structure
) и перейти к разбору FUN_80011f0c
.
Я немного переименовал входные аргументы, чтобы было понятнее, остаётся разобраться, что здесь делается.
Во первых, здесь осуществляется проход по структуре mr_sub_data
(это та, которая начинается со смещения +0x04
— не +0x02
потому что short*
— от начала mr_mem
, т.е. от начала MR-файла). Во вторых, да, mr_sub_data
это структура, поэтому давайте её создадим. Почему-то, в этот раз Ghidra с автоматическим созданием не справилась, создав слишком короткую структуру, поэтому сделаем это вручную (просто добавляем по байтовому полю, пока не пропадут обращения к указателю со структурой как к массиву):
Обратив внимание, что в текущей функции используется рекурсия при вызове FUN_80011f0c
, решаем разобраться сразу с тем блоком кода, в котором она имеется, а именно:
В блоке с рекурсией есть всего одна неизвестная функция, поэтому мы сразу перешли в неё.
Выглядит достаточно просто: имеется множество обращений к полям структуры astruct_1
, (которая, напоминаю, является списком), а также очевидно формирование новой подструктуры того же типа. Изменим типы полей и переименуем их так, чтобы итоговый код функции смотрелся приятнее, и получим следующее:
Теперь нас ждёт второй блок кода, в котором рекурсии не было. Переходим в единственную вызываемую из него функцию. Код оказался слишком большим, поэтому пришлось спрятать его под спойлер:
/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */
undefined4 FUN_80011808(astruct_1 *item,char *param_2,int param_3,undefined2 param_4,size_t param_5,size_t param_6,void *param_7,int index)
{
dword dVar1;
int iVar2;
int iVar3;
undefined4 uVar4;
undefined *puVar5;
char *dst;
int iVar6;
dword dVar7;
void *pvVar8;
dword dVar9;
undefined local_res8;
char local_230 [512];
size_t local_30;
size_t local_2c;
iVar2 = strlen(param_2);
iVar6 = iVar2 + 1;
iVar3 = iVar6;
if (iVar6 < 0) {
iVar3 = iVar2 + 4;
}
iVar3 = iVar6 + (iVar3 >> 2) * -4;
if (0 < iVar3) {
iVar6 = (iVar2 + 5) - iVar3;
}
pvVar8 = (void *)0x0;
if (*(code **)(&DAT_800595a0 + param_3 * 4) == (code *)0x0) {
if ((int)param_6 < 1) {
puVar5 = (undefined *)malloc2(iVar6 + param_5 + 8);
FUN_80017e5c(puVar5 + iVar6 + 8,param_7,param_5);
dst = puVar5 + 8;
}
else {
local_2c = param_6;
puVar5 = (undefined *)malloc2(iVar6 + param_6 + 8);
FUN_8001441c(puVar5 + iVar6 + 8,&local_2c,param_7,param_5);
dst = puVar5 + 8;
}
}
else {
local_230[0] = '';
memset();
FUN_800127a4(_DAT_00000170,item,local_230);
iVar3 = strlen(local_230);
local_230[iVar3] = '/';
local_230[iVar3 + 1] = '';
strcat(local_230,param_2);
if (0 < (int)param_6) {
local_30 = param_6;
pvVar8 = malloc(param_6);
FUN_8001441c(pvVar8,&local_30,param_7,param_5);
param_5 = param_6;
param_7 = pvVar8;
}
uVar4 = FUN_80012aa0(index);
iVar3 = (**(code **)(&DAT_800595a0 + param_3 * 4))(local_230,param_3,param_5,param_7,uVar4);
if (iVar3 == 1) {
if (pvVar8 == (void *)0x0) {
return 1;
}
FUN_80017d60(pvVar8);
return 1;
}
puVar5 = (undefined *)malloc2(iVar6 + param_5 + 8);
FUN_80017e5c(puVar5 + iVar6 + 8,param_7,param_5);
dst = puVar5 + 8;
if (pvVar8 != (void *)0x0) {
FUN_80017d60(pvVar8);
dst = puVar5 + 8;
}
}
strcpy(dst,param_2);
local_res8 = (undefined)param_3;
puVar5[1] = local_res8;
*(undefined2 *)(puVar5 + 2) = param_4;
*puVar5 = (undefined)index;
dVar9 = 0;
dVar1 = item->field_0x4;
if (item->field_0x4 == 0) {
item->field_0x4 = (dword)puVar5;
}
else {
do {
dVar7 = dVar1;
iVar3 = strcmp(param_2,(char *)(dVar7 + 8));
if (iVar3 == 0) {
*(undefined4 *)(puVar5 + 4) = *(undefined4 *)(dVar7 + 4);
if (dVar9 == 0) {
item->field_0x4 = (dword)puVar5;
}
else {
*(undefined **)(dVar9 + 4) = puVar5;
}
*(undefined4 *)(puVar5 + 4) = *(undefined4 *)(dVar7 + 4);
FUN_80017d60(dVar7);
return 0;
}
if (iVar3 < 0) {
if (dVar9 == 0) {
item->field_0x4 = (dword)puVar5;
}
else {
*(undefined **)(dVar9 + 4) = puVar5;
}
*(dword *)(puVar5 + 4) = dVar7;
goto LAB_80011af0;
}
dVar1 = *(dword *)(dVar7 + 4);
dVar9 = dVar7;
} while (*(dword *)(dVar7 + 4) != 0);
*(undefined **)(dVar7 + 4) = puVar5;
}
*(undefined4 *)(puVar5 + 4) = 0;
LAB_80011af0:
item->field_0x2 = item->field_0x2 + 1;
return 1;
}
Думаю, на этом моменте можно остановиться, т.к. код здесь довольно объёмный, а дальше будет только больше. Во второй (и, возможно, третьей) части у нас будет:
zlib
На этом всё.
OpenAI готовится запустить собственную поисковую систему на базе ChatGPT. Информацию об этом публикуют западные издания. Ожидается, что новый поисковик может…
Центр управления связью общего пользования (ЦМУ ССОП) Роскомнадзора рекомендовал компаниям из реестра провайдеров ограничить доступ поисковых ботов к информации на российских сайтах.…
Apple возобновила переговоры с OpenAI о возможности внедрения ИИ-технологий в iOS 18, на основе данной операционной системы будут работать новые…
Конкурсный управляющий российской «дочки» Google подготовил 23 иска к участникам рекламного рынка. Общая сумма исков составляет 16 млрд рублей –…
Google завершил обновление основного алгоритма March 2024 Core Update. Раскатка обновлений была завершена 19 апреля, но сообщил об этом поисковик…
У частных продавцов на Авито появилась возможность составлять текст объявлений с помощью нейросети. Новый функционал доступен в категории «Обувь, одежда,…