Разбираем ресурсы Twisted Metal 4 (PSX) в Ghidra. Часть 1

Всем привет,

В данной статье я расскажу о реверс-инжиниринге ресурсов игры Twisted Metal 4 для первой Playstation. В качестве основного инструмента я буду использовать Ghidra.

Наверняка вы слышали об игровой серии Twisted Metal. А кому-то, наверное, довелось и поиграть (мне нет). По словам тех, кто играл в четвёртую часть, в игре имеются некоторые неприятные баги. Так вот, реверс-инжиниринг должен помочь исправить их. Поехали…

Игровые форматы

Как обычно, разбор игры для первой плойки начинается с анализа основного исполняемого файла, в данном случае — SCUS_945.60. После непродолжительного осмотра данного файла, я пришёл к выводу, что в нём, фактически, большей части кода-то и нет. Значит, всё упрятано куда-то ещё.

На диске, кроме основного исполняемого файла, обнаружились следующие форматы: IMG, MR, STR, TIM. Два последних являются стандартными для PSX, и представляют из себя видео-стримы и изображения соответственно. А вот на первые два надо бы взглянуть. Открываем в hex-редакторе первый попавшийся файл и видим:

Судя по “плотности” байт, файл чем-то упакован. Значит, нужно понять, где и как происходит распаковка.

Загрузка SCUS_945.60 в Ghidra

Для работы с исполняемыми файлами Sony Playstation 1 в Ghidra имеется загрузчик, я писал о нём здесь. Устанавливаем его, открываем Гидру, создаём новый проект и закидываем в него файл SCUS_945.60 из корня диска с игрой.

Узнаём, что при разработке игры использовался PsyQ 4.5.

Дожидаемся завершения анализа файла, и переходим в автоматически определившуюся функцию main().

Я выделил на скриншоте функцию, которая бросается в глаза — похоже, она имеет какое-то отношение к разбору MR-файлов. Посмотрим, что она делает.

FUN_800217e8

В вызове этой функции в main() мы видели, что первым аргументом туда приходит имя файла, поэтому меняем тип первого аргумента на соответствующий — char * (у Гидры понятий const, не const нет), и переименовываем в mr_name.

Далее видим, что имя файла передаётся в следующую функцию:

iVar1 = FUN_80021094(mr_name,&local_8);

Зайдём в неё:

FUN_80021094

Здесь так же переименовываем и меняем тип аргумента. Первой же строчкой в этой функции видим использование какой-то переменной.

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. Посмотрим, что она делает.

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_800217e8

Теперь данная функция выглядит следующим образом.

Видим вызов функции FUN_80021690, в которую передаются “координаты” файла, его имя, и второй аргумент функции. Зайдём и посмотрим.

FUN_80021690

Тут, вроде как всё просто и интуитивно, если учитывать, что мы имеем дело с координатами файла, его позицией и т.п. Делаю предположение, что первая вызываемая функция — FUN_80017cf8 — выделяет память под содержимое файла, которое будет прочитано далее. А вторая — собственно чтение. Для проверки я заглянул в первую функцию, и ужаснулся — значит, это точно выделение памяти! Посмотрим на вторую.

Да, очень похоже на чтение с диска. Переименовываем её в read_data_by_pos(). Поднимаемся выше, и переименовываем функцию FUN_80021690 в read_file_with_alloc(). Не забываем нажимать P для применения параметров.

FUN_800217e8

Возвращаясь снова к этой функции, можно смело назвать её read_file(), и определить второй аргумент функции, как offset, с которого начинается чтение файла, ибо указатель на выделенную память занят за результатом функции, который она возвращает через return.

main

И вот, мы снова вернулись в главную функцию, имея указатель на прочитанные из файла MAIN.MR данные.

Видим, что функция FUN_80012bf0 принимает этот указатель вторым аргументом. А первым снова идёт какая-то переменная (или константа). Уже имея в этом опыт, выясняем, что это — строка “/“. Давайте теперь посмотрим, что же делает FUN_80012bf0.

FUN_80012bf0

Ясности первый осмотр функции, в то что она делает, не внёс. Зайдём в FUN_800128cc, которая, судя по листингу, не принимает аргументов и разберём её.

FUN_800128cc

Аргументы у этой функции всё таки есть. Нажмём P. Видим что имеется вызов strcpy, но, почему-то с нераспознанными аргументами, исправим:

Зайдём в первую вызываемую функцию, куда передаётся первый аргумент, имеющий теперь тип char *:

iVar1 = FUN_80012a24(param_1);

FUN_80012a24

Снова библиотечная функция, но на этот раз — strcmp(), добавим ей аргументы.

Становится понятно, что мы имеем дело со списком строк (DAT_8005963c), и их 16 штук (судя по условиям цикла). Исправим тип для указанного адреса на char*[16]. Для этого сначала перейдём по адресу 0x8005963C, зададим тип char* для первого указателя (через T), а затем нажмём клавишу [, чтобы создать массив.

Теперь функцию FUN_80012a24 можно переименовывать в find_prev_string_index(). Возвращаемся назад.

FUN_800128cc

Теперь мы видим довольно интересный кусок кода. Опытный глаз сразу определяет подсчёт длины strlen(), и выделение памяти под новую строку (ещё один malloc()?).

Но давайте проверим. Заходим в первую вызываемую функцию — FUN_8003d7a8:

Тут так и написано: “Possible STRLEN.OBJ/strlen“. Почему же анализатор автоматически не применил сигнатуру, спросите вы? Варианта может быть два:

  1. Ранее нашлась совпадающая по сигнатуре функция
  2. Энтропия сигнатуры не превысила минимально допустимой, заданной в настройках (минимум 3.0)

Ну а вторая функция (FUN_80017cd8) оказалось такой же объёмной внутри, как и ранее найденный malloc(), поэтому смело называем данную функцию malloc2().

Итого, вся функция FUN_800128cc — это что-то типа strdup() с кэшированием. Переименовываем, и возвращаемся наверх.

FUN_80012bf0

Теперь начинается что-то интересное. Смотрим первый вызов:

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

Снова поднимаемся наверх, и смотрим, что теперь представляет из себя функция 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.

FUN_80011f0c

Я немного переименовал входные аргументы, чтобы было понятнее, остаётся разобраться, что здесь делается.

Во первых, здесь осуществляется проход по структуре mr_sub_data (это та, которая начинается со смещения +0x04 — не +0x02 потому что short* — от начала mr_mem, т.е. от начала MR-файла). Во вторых, да, mr_sub_data это структура, поэтому давайте её создадим. Почему-то, в этот раз Ghidra с автоматическим созданием не справилась, создав слишком короткую структуру, поэтому сделаем это вручную (просто добавляем по байтовому полю, пока не пропадут обращения к указателю со структурой как к массиву):

Обратив внимание, что в текущей функции используется рекурсия при вызове FUN_80011f0c, решаем разобраться сразу с тем блоком кода, в котором она имеется, а именно:

FUN_80011bd0

В блоке с рекурсией есть всего одна неизвестная функция, поэтому мы сразу перешли в неё.

Выглядит достаточно просто: имеется множество обращений к полям структуры astruct_1, (которая, напоминаю, является списком), а также очевидно формирование новой подструктуры того же типа. Изменим типы полей и переименуем их так, чтобы итоговый код функции смотрелся приятнее, и получим следующее:

FUN_80011808

Теперь нас ждёт второй блок кода, в котором рекурсии не было. Переходим в единственную вызываемую из него функцию. Код оказался слишком большим, поэтому пришлось спрятать его под спойлер:

Код FUN_80011808
/* 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;
}

Конец первой части

Думаю, на этом моменте можно остановиться, т.к. код здесь довольно объёмный, а дальше будет только больше. Во второй (и, возможно, третьей) части у нас будет:

  • Дальнейший разбор MR-формата
  • Сжатие zlib
  • Парсер MR-формата на Python
  • Разбор собственного формата PIC-кода (Position Independent Code) с релокациями, импортами и блекджеком. Напоминаю, что в MIPS все адреса вызовов должны быть абсолютные
  • Загрузчик MR-формата для Ghidra

На этом всё.

Let’s block ads! (Why?)

Read More

Recent Posts

OpenAI готовится запустить поисковую систему на базе ChatGPT

OpenAI готовится запустить собственную поисковую систему на базе ChatGPT. Информацию об этом публикуют западные издания. Ожидается, что новый поисковик может…

47 минут ago

Роскомнадзор рекомендовал хостинг-провайдерам ограничить сбор данных с сайтов для иностранных ботов

Центр управления связью общего пользования (ЦМУ ССОП) Роскомнадзора рекомендовал компаниям из реестра провайдеров ограничить доступ поисковых ботов к информации на российских сайтах.…

22 часа ago

Apple возобновила переговоры с OpenAI и Google для интеграции ИИ в iPhone

Apple возобновила переговоры с OpenAI о возможности внедрения ИИ-технологий в iOS 18, на основе данной операционной системы будут работать новые…

6 дней ago

Российская «дочка» Google подготовила 23 иска к крупнейшим игрокам рекламного рынка

Конкурсный управляющий российской «дочки» Google подготовил 23 иска к участникам рекламного рынка. Общая сумма исков составляет 16 млрд рублей –…

6 дней ago

Google завершил обновление основного алгоритма March 2024 Core Update

Google завершил обновление основного алгоритма March 2024 Core Update. Раскатка обновлений была завершена 19 апреля, но сообщил об этом поисковик…

6 дней ago

Нейросети будут писать тексты объявления за продавцов на Авито

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

6 дней ago