Створення інструментів для дослідження NES-ігор

Створення інструментів для дослідження NES-ігор

Я вирішив продовжити серію публікацій про внутрішній пристрій NES-ігор, на цей раз я розповім про використовувані мною інструменти для досліджень.

Велика частина того, що необхідно досліднику, вже є в емуляторі FCEUX, який добре підходить для налагодження ігор. У документації варто досконально вивчити розділ Debug, кожен інструмент звідти корисний досліднику, причому вміння використовувати їх спільно один з одним посилює можливості хакера.

Однак я не буду переказувати документацію, а зупинюся на випадках, коли можливостей емулятора виявляється мало і необхідно додати нові, або ж коли існують незвичайні способи знайти бажане в ROM-файлі безпосередньо, в обхід тривалого вивчення коду ігор.

Використання скриптів Lua

Власне, перший спосіб, приклад якого наведено на картинці для привернення уваги - створення допоміжних інструментів за допомогою вбудованого в емулятор інтерпретатора Lua-скрипт.

На прикладі вище для дослідження гри (і просто читерського проходження, при бажанні) використовується така можливість скриптів, як виведення зображень на екран поверх вимальовуваної емулятором картинки.

Таким чином дослідник може помітити те, що недоступно звичайному гравцеві, наприклад, на скріншоті вище з трьох прихованих діамантів гравець може дострибнути тільки до перших двох, і ніяким чином не може взяти третій або навіть просто здогадатися про його існування. У «Качиних історіях 2» зустрічаються навіть такі коштовності, які взагалі розміщені за межами ігрового рівня.

Інший приклад скрипту з виведенням на екран додаткових даних - компас до найближчого дорогоцінного каменю в «Книзі Джунглів»:

Природно, візуалізація інформації з оперативної пам'яті або ROM гри не єдина можливість скриптів.

Інша часто використовувана можливість - логгування того, що відбувається в коді гри, наприклад, шаблон скрипту для дампа розархівованих даних відразу після їх розпакування (для SMD ігор, але принцип застосовний і для NES).

Ну, і ніхто не забороняє створення на Lua-скриптах повноцінних утиліт, начебто вже включеного в емулятор редактора натиснутих клавіш TasEditor.

Також, на мій погляд, недооціненою є ідея часткового переписування коду гри на скриптах, коли ігрові дані патчаться скриптом на льоту для модифікації геймплея. Proof-of-concept такого скрипту, що модифікує ворогів у «New Ghostbusters 2»:

Однак, для складної обробки конкретної гри або створення нових методів хаку варто задуматися про використання наступного методу.

Модифікація початкового коду емулятора

Тут є де розгулятися фантазії на різні теми, що не стосуються дослідження ігор, на кшталт додавання в емулятори підтримки досягнень, 3d-рендера або поліпшеної графіки, однак я спробую втриматися в рамках теми статті.

Один з напрямків розширення емулятора з метою поліпшення можливостей для реверс-інженерії - прокидання якомога більшого числа його внутрішніх можливостей в Lua-бібліотеки. У другій статті циклу я вже показував, як за допомогою прокидання всього пари нових функцій з'явилася можливість зробити універсальний (відповідний для дослідження будь-якої гри) інструмент для дослідження.

Інший простий і корисний приклад, який поки ще відсутній в останній версії емулятора - можливість модифікації зі скрипту пам'яті PPU.

Модифікація емулятора може також використовуватися для того, щоб вбудувати в нього редактор для конкретної гри з можливістю на льоту запустити її і перевірити внесені зміни:

Скрипти для статичного аналізу коду гри

Попередні дві категорії модифікацій стосувалися динамічного аналізу гри в ході її виконання. Однак велика частина дослідження - це статичний аналіз ROM-файлу гри (або дампів будь-яких даних з нього).

Основною програмою для такого аналізу коду є інтерактивний дизассемблер IDA. Він підтримує асемблер 6502, однак вимагає як плагіну для правильного завантаження файлів у форматі nes, так і набору скриптів для автоматизації рутинних дій з перетворення завантаженого файла на причісаний код. Набір скриптів, специфічних для дослідження NES-ігор зібрано тут.

Самі скрипти для IDA можуть бути написані вбудованою мовою команд idc або python, в будь-якому випадку найкраще відкрити їх текстовим редактором і вивчити, в більшості випадків це допомагає краще зрозуміти команди самого IDA, які стануть в нагоді в роботі з ним і навчитися писати такі скрипти самому. Це дуже знадобиться, коли знадобиться провести кілька сотень однотипних дій, на зразок об'єднання байт у поінтери або виділення масивів за деякими правилами.

Інструменти для статичного аналізу даних гри

IDA хороший інструмент для аналізу коду, настільки хороший, що деякі гуру дослідження ігор навіть вважають, що тільки його достатньо для дослідження і зміни ігор. Однак, навіть маючи на руках розібрану до компільованих і прокоментованих вихідців гру, складно модифікувати ігрові дані - рівні, графічні карти, анімації персонажів. На жаль, формат ігрових даних часто сильно відрізняється від гри до гри, тому створити універсальні інструменти, що підходять для більшості ігор, досить важко.

Редактори тайлових карт

Формат зберігання графічних банків (найнижчий рівень зберігання графіки) стандартний для всіх ігор NES, тому існує безліч редакторів тайлових карт, однак, серед них я не знайшов жодної бібліотеки, яка дозволяла б рендерити ці тайли в своєму додатку.

Такими програмами можна редагувати тайли графіки в іграх з наявністю CHR-ROM - цілими банками графіки. В інших іграх використовується CHR-RAM - відеопам'ять тайлів в них зчитується частинами з банку з даними і кодом і копіюється у відеопам'ять (при цьому іноді досить хитрими способами, але про них швидше краще розповідати в статті про компресію даних).

На більш високому рівні ігри відрізняються вже настільки, що загальних програм для редагування практично немає, максимум існують редактори, що охоплюють кілька ігор на одному движку. Про свої спроби зробити універсальний редактор рівнів я напишу в кінці статті, поки ж наведу ще кілька загальних ідей, як знаходити дані в іграх, і утиліт, що реалізують ці ідеї.

Як мову реалізації я використовую python за те, що на ньому можна швидко і просто перевірити будь-яку здогадку, іноді навіть прямо в інтерактивному режимі.

Коррапт ROM

Власне, якраз про цю ідею була друга стаття циклу - якщо перебрати всі можливі варіанти зміни одного байту в ROM і подивитися, як це позначиться на екрані, то це може допомогти прояснити внутрішню структуру гри. Після цього можливо навіть скласти простий варіант редактора гри - потрібно заготовити набір картинок-блоків верхнього рівня, з яких будується екран, не вникаючи до кінця, як будуються самі ці картинки з даних ROM і відобразити масив цих картинок, виявлений цим методом.

Пошук блоків

Можна також зайти з іншого боку.

Тло, яке відображається на екрані, задається масивом індексів тайлів відеопам'яті за фіксованою адресою PPU - для NES існує 4 екранні сторінки, які залежно від налаштувань PPU можуть різними способами виводитися на екран. Не важливо, що саме буде на екрані, достатньо просто захопити якусь завантажену сторінку для аналізу.

Перша екранна сторінка (Name Table) розташована за адресами PPU $2000- $23BF. Її вміст у емуляторі FCEUX можна переглянути у вікні Debug  Name Table Viewer:

А також у вигляді байт у вікні Debug  Hex Editor, View  PPU Memory (перейти за адресою $2000).

Тут же можна зробити дамп всієї відеопам'яті, який стане в нагоді нам для аналізу (File  dump to File  PPU Memory).

Це просто масив 960 індексів маленьких тайлів відеопам'яті розміром 8x8 пікселів. При цьому, після реверсу великої кількості ігор відомо, що ігрові екрани часто описуються блоками більшого розміру, наприклад, 16x16 або 32x32 пікселів. Таким чином, якщо ми припустимо певний розмір блоку (для початку спробуємо найбільш стандартні - 2x2 тайли, виділені на скріншоті червоною рамкою), то ми можемо розбити дані з екранної сторінки на ділянки, в кожній з яких виявиться опис одного блоку.

Так виходить список з усіх блоків, які присутні на екрані. Причому у нас «чисті» описи блоків, без інформації про спрайти персонажів (спрайти малюються іншим способом), і незалежний від анімації (анімації фону практично завжди робляться за допомогою змін палітри або самої відеопам'яті, номери тайлів в Name Table залишаються незмінними). Однак ми не знаємо номери блоків.

У нас є опис блоків на екрані, але ми не знаємо їх порядок зберігання в ROM. Тим не менш, ми можемо з деякою ймовірністю припустити, де саме розташований опис блоків. Алгоритм для цього такий:

1. Проходимо по всьому ROM і розмічаємо всі адреси, за якими виявляється який-небудь блок, при цьому зберігаємо його номер (справжній номер може бути інший, нам важливо відзначити тільки відмінності блоків один від одного).

2. Знаходимо область в ROM, в якій виявлено найбільшу кількість РІЗНИХ блоків. З найбільшою ймовірністю саме це і є опис блоків.

https://gist.github.com/spiiin/500262e8d9da86f10a093bbb41833360

Таким чином, ми можемо знайти блоки розміром 2x2 в іграх, в яких вони зберігаються послідовно.

Це вже непогано, але є спосіб кардинально поліпшити результати роботи алгоритму. Справа в тому, що існує обмежена кількість основних розмірів блоків і способів їх зберігання в ROM, і ми можемо перебрати їх всі.

Основні розміри блоків: 2x2, 4x2, 2x4 і 4x4, але в разі потреби легко додати й інші розміри.

Зі способом зберігання їх в ROM трохи хитріше, блоки можуть зберігатися як лінійно, так і розбитими на частини масивами (Structure of Arrays, скорочено SoA), тобто спочатку в ROM зберігається масив тільки перших частин блоків, за ним - масиви з наступними частинами. Найчастіше такі масиви зберігаються один за одним, при цьому проміжок між початками масивів дорівнює кількості блоків. Щоб знайти в ROM такі SoA-масиви, ми повинні дізнатися їх довжину, що можна зробити перебором всіх варіантів (частенько в іграх використовується по 256 блоків, так що починати перевірку варто з цього числа і поступово його зменшувати).

Все це виглядає досить заплутано, адже ми спираємося тільки на ймовірність того, що гра використовує певний вид блоків, але на практиці утиліту знаходить блоки в 80-90% перевірених ігор!

https://github.com/spiiin/NesBlockFinder

На додачу, вона дозволяє відсіяти ігри з незвичайною структурою (неблоковою), щоб пильніше їх вивчити.

Порівняння файлів CDL

Емулятор FCEUX вміє під час емуляції кожну інструкцію зазначати, які байти були інтерпретовані як код, а які - як дані (меню Debug ^ Code/Data Logger...). Ця фіча корисна сама по собі і тісно інтегрована з іншими зневаджувальними можливостями емулятора - спробуйте увімкнути цей режим і подивитися, як змінилися інші зневадочні вікна. Однак, я хочу розповісти про її одне приватне застосування. Якщо зберегти два таких cdl-файли, один ДО вчинення дії, а інший відразу ж ПІСЛЯ його закінчення, то різниця між двома такими файлами покаже тільки ті дані (або код), які були використані під час вчинення дії. При грамотному відсіканні можна знайти потрібні дані, лише правильно вибравши два моменти часу між вимірюваними подіями.

Формат файлу досить простий, як і скрипт порівняння, але користі він може принести дуже багато, на ньому взагалі можна побудувати окрему методологію зневадження.

Компресори/декомпресори

Цю тему неможливо розкрити в парі абзаців, та й вона буде занадто спрощеною в контексті тільки NES-ігор, тому вона заслуговує окремої статті.

Універсальний редактор рівнів CadEditor

Власне, спочатку ця програма створювалася для відображення рівнів у грі «Chip & Dale» (Chip And Dale Editor), далі на прохання вона була перероблена в редактор і з часом обростали підтримкою інших ігор Capcom («Darkwing Duck», «Duck Tales 1-2», «Tale Spin», «», Lid «Title».

Пізніше з'ясувалося також, що принципи блочної побудови рівнів в цих іграх дуже схожі на способи організації рівнів з багатьох інших ігор, однак диявол криється в деталях - дрібні відмінності в кожній грі вимагають описувати внутрішні структури редактора максимально гнучко, щоб за допомогою комбінацій цих структур можна було описати формат рівнів для ігор, для яких він спочатку не призначався, без зміни ядра самого редактора.

Фіча, за допомогою якої забезпечується універсальність редактора - так звані конфіги ігор. Це скриптові файли мовою C #, в яких описано, як саме завантажувати дані конкретної гри. Чому саме C #? Редактор вже був написаний цією мовою і це дозволило легко переносити код з ядра в конфіги, не змінюючи його, що довелося б робити, якби використовувалася більш класична скриптова мова, на кшталт Lua.

Використання повноцінної мови замість простого файла налаштувань дозволяє описувати свої функції завантаження і збереження даних будь-якої необхідної складності. Скрипти - це звичайні текстові файли, що дозволяє користувачам у разі необхідності створювати свій налаштування без перекомпіляції редактора, використовуючи вже існуючі конфіги як шаблони. У комплекті з редактором йдуть близько 500 конфігів для 60 різних ігор, близько 100 з них зроблені користувачами редактора без моєї участі, для ігор, в деякі з яких я навіть ніколи не грав:

Однак в даний момент, незважаючи на спроби зробити універсальний редактор, виявляються ігри, які неможливо описати без модифікації самого редактора (тим не менш, безліч ігор вже можна додати і так). Щоб збирати інформацію про такі ігри з незвичайною структурою, я пішов ще трохи далі і почав використовувати сам редактор як бібліотеку для Python, щоб дослідити формат ігор перед додаванням їх в редактор і тестувати правильність розуміння побудови рівня конкретної гри. Я реалізував це у вигляді блокнотів Jupyter, через те, що в них зручно як писати код в інтерактивному режимі, так документувати його.

Складання з базових тайлів великих ігрових структур і складання в підсумку цілого рівня нагадує збірку пазла з тисяч шматочків, і доставляє таке ж задоволення, коли, нарешті, кожен шматочок опиняється на своєму місці.

У наступній статті не буде такої великої кількості технічної інформації і я наведу приклади збирання рівнів ігор з нестандартною структурою або використанням незвичайних модифікацій стандартної блокової архітектури. Також ви можете в коментарях назвати гру на NES, формат рівнів якої цікавий вам, можливо, я досліджую і її теж.

Image