Как стать автором
Обновить

История взлома классической игры на Dendy или Contra на 100 жизней

Время на прочтение7 мин
Количество просмотров12K

Поскольку некоторая японская компания, до сих пор тщательно бдит свои авторские права. Я не могу предоставить вам ни мою версию рома ни использованный мною исходник. Скажу лишь, что нашёл его в торрент сборнике "Все игры на Dendy". Взяв оттуда переведённую на русский язык японскую версию игры "Contra (J) [T+Rus_Chronix]" я несколько раз её прошёл и будучи крайне любознательным человеком решил немного расковырять ROM образ, в частности подарить немного жизней игрокам.
Для взлома игры я воспользовался эмулятором FCEUX.


И это все предварительные приготовления, не считая настроек экрана и геймпада.


Сам взлом проходит в несколько этапов:


  1. Поиск нужных значений, а точнее хранящих их переменных, а точнее адресов содержащих нужные значения
  2. Поиск инструкций меняющих или читающих эти значения, в зависимости от того что вас интересует
  3. Анализ блоков кода содержащих эти инструкции
  4. Изменение интересующих блоков кода, с последующей записью в ROM-файл

При чём, если результатом анализа блоков кода будут другие адреса пункты 2 и 3 придётся повторять. Дальше я этот момент покажу.


Для поиска значений в памяти используется встроенный в эмулятор инструмент "RAM search".



Я не знаю использовали ли в играх для NES шифрование, мне такие игры до сих пор не попадались. А на экране после заставки и перед началом уровня я увидел количество жизней "три".



Вот с тройки я и начал поиск.


Оператор сравнения "equals to", сравниваю с определённым значением "3" и не стоит нажимать на кнопку поиска до начала уровня. По идее для NES игры это не важно, но я предпочитаю перебдеть и начать поиск после того как буду точно знать, что искомое значение лежит на своём месте в памяти, то есть после начала самого уровня.


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


Но у меня после этого переменных не осталось и причин у этого может быть две:


  1. человеческий фактор, проще говоря я ошибся в процессе поиска
  2. в переменной отвечающей за жизни лежит что-то другое

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


И прежде чем продумывать план обхода хитрого шифрования посмотрите на экран ещё раз.



Жизней у игрока три, а медалей над ним висит две. Возможно когда у игрока три жизни в памяти записано значение "два" (две запасных жизни). Сбросив игру и начав новый поиск, уже с количеством медалей и двумя игроками я почти сразу нашёл два адреса с подходящими значениями.



Это 3216 и 3316 адреса. Самый простой способ проверить найденный адрес — поменять его значение. Для этого нужен встроенный в эмулятор инструмент "HEX Editor" при чём в окне "RAM Search" его можно открыть правым кликом по адресу, так вы откроете именно этот адрес в памяти. И поставив в 3216 адрес "четвёрку" я увидел как поменялся запас первого игрока, а поставив её же в 3316 адрес я поменял жизни второго игрока.


Самая скучная и рутинная часть работы выполнена. Кнопкой "Watch" можно добавить адрес в окно "RAM Watch" созданное для наблюдения за значениями, а в моём случае чтобы, компьютер помнил нужные мне адреса, у меня память дырявая, я забуду. Кстати, эти переменные хранятся в статической памяти эмулятора и найдя их раз вы можете всегда ими пользоваться, не боясь перезапускать игру, эмулятор и даже компьютер.


На втором этапе нужно искать функции/субрутины/блоки кода (любимое подчеркнуть) меняющие эти значения. Если игру создавали не дураки, а так и есть, в начале игры жизни обоим игрокам выдает одна субрутина, но не зная этого заранее такие вещи лучше проверять. Для этих целей в эмулятор встроен отладчик, окно "Debugger".



И это наверно самое сложное окно в эмуляторе. Формально его можно разбить на две части большое поле слева показывает всё содержимое памяти разбивая его на опкоды с параметрами/аргументами и рядом пишет соответствующие им мнемоники. А в правой половине на первый взгляд сам чёрт ногу сломит. Хотя там нет ничего сложного нужно лишь всё внимательно прочитать и по возможности запомнить. Меня тут в первую очередь интересует поле "Breakpoints" куда кнопкой "Add" можно добавлять точки останова на адресах. Чтоб узнать какие инструкции пишут данные в эти адреса я установил галочку на "Write".



Настройка готова.


Можно перезапустить игру и посмотреть какие инструкции, из каких субрутин и какие значения пишут в этот адрес. Помня, что меня интересует запись в этот адрес двойки, можно смело игнорировать все субрутины записывающие туда нули. Для продолжения программы после остановки следует нажать кнопку "Run" в окне отладчика.


И вот наконец, интересный кусок кода.


Address Opcode Mnemonic Arguments A X Y
C2E7 85 39 STA $0039 ?? 00 ??
C2E9 A9 02 LDA #$02 ?? 00 ??
C2EB A4 24 LDY $0024 02 00 00
C2ED F0 02 BEQ $C2F1 02 00 00
C2EF A9 1D LDA #$1D 02 00 00
C2F1 95 32 STA $32,X 02 or 1D 00 00
C2F3 CA DEX 02 or 1D 00 00
C2F4 10 F3 BPL C2E9 02 or 1D FF 00
C2F6 A9 C8 LDA #$C8 02 or 1D FF 00
C2F8 85 3C STA $003C 02 or 1D FF 00

Вычеркнутые инструкции меня не интересуют, т.к. они работают уже с совершенно другими адресами. И я детально рассмотрю интересующие меня инструкции. В адресе C2E916 лежит опкод A916 записывающий значение в регистр A, ему соответствует человекопонятное сокращение LDA "LoaD A", означающее записать в регистр A. Этому сокращению соответствуют восемь опкодов и конкретно A916 значит, что в следующем за инструкцией байте записано само значение. Помимо самого значения может быть передан адрес откуда следует взять значение. Боле подробно об опкодах можно прочитать здесь, о мнемониках (человекопонятных сокращениях) здесь, а детально изучить данный assembly language здесь (хотя зачем?). Теперь вы вооруженные знаниями можете расшифровать остальные инструкции. Только отладчик в эмуляторе немного лукавит и в адресе C2EB16 показывает инструкцию LDY $0024 а на самом деле эта инструкция пишется как LDY $24, значат обе этих записи одно и то же, но первая переведённая в опкод займёт на один байт больше и будет выглядеть как AC 24 00. К инструкции по адресу C2ED16 тоже могут возникнуть вопросы, это инструкция ветвления срабатывающая при равенстве, но перед ней ничего не сравнивается, как же это работает? На самом деле очень просто, данная инструкция читает только Zero flag статусного регистра. Инструкция сравнения в ассембли работает через вычитание одного значения из другого без записи результата куда либо, но с изменением флагов статусного регистра. Потому инструкции ветвления читают только флаги. В данном случае инструкция LDY из адреса C2EB16 кладёт в регистр Y ноль записанный ранее в адрес 002416 и кладет в Zero Flag единицу, потому что результатом последней операции был ноль. А инструкция ветвления игнорирует всё кроме состояния Zero флага и увидев в нём единицу прыгает на 2 байта вперёд, от начала следующей за ней инструкции, то есть в адрес C2EF16 + 216 = C2F116. В этом адресе в регистре A лежит 216 или 1D16 в зависимости от того был совершён прыжок или нет, прыжок был и потому в регистре лежит двойка. Она и попадает в адрес 3216 с отступом в значение регистра X, то есть 003216 + 0016 = 003216. Следующая инструкция DEX уменьшает значение регистра X на 1 записывая в него значение FF16. Помним что в 8 битной знаковой арифметике значения (0016 — 7F16) — положительные, а значения (8016 — FF16) — отрицательные. Именно знак числа проверяет инструкция BPL и если число положительное возвращает выполнение в адрес C2E916. F316 = -1310 = -D16, C2F616 — D16 = C2E916. Здесь можно предположить, что где то ранее в коде в регистр X записывалось 0 если игра на одного игрока или 1 если игра на двоих. Очевидно что второй игрок уже получил свою порцию жизней на предыдущей итерации цикла (C2E916 — C2F416), и монитор адресов это показывает, в адресе 003316 лежит двойка. Просмотрев код выше в адресе C2DD16 видно запись в регистр X значения из адреса 002216, если установить на этот адрес брейкпоинт на запись и проверить когда и что туда записывается, можно увидеть что значение в этом адресе и правда меняется на 0 если игрок один и на 1 если игроков двое. Этот момент я разобрал. Теперь надо разобраться с записью. Очевидное на первый взгляд решение передать в адресе C2E916 к опкоду A9 аргумент 9910 = 6316, но если игрок введёт "Конами код"?


Пришлось идти по более сложному пути.


Идея такова: введённый "Конами код" нельзя отменить, а значит можно убедить игру, что код уже введён, а значение 9910 вписать в аргумент инструкции из адреса C2EF16, той что срабатывает при введённом "Конами коде". Так я гарантированно впишу обоим игрокам по 9910 запасных жизней, независимо от того вводили они код или нет. Звучит достаточно просто и я начал с анализа адреса 002416, того откуда игра считывает "Конами код", установив брейкпоинт на запись в этот адрес я попал в следующий блок кода.


Address Opcode Mnemonic Arguments A X Y
F95B 91 00 STA ($00),Y 00 ?? 24 or 23… or FF
F95D 88 DEY 00 ?? 24 or 23… or FF
F95E C0 FF CPY $FF 00 ?? 23 or 22… or FF
F960 D0 F9 BNE $F95B 00 ?? 23 or 22… or FF

Внимательно его прочитав я пришёл к выводу, что эта субрутина зануляет диапазон адресов, а значит мне не подходит. Больше этот адрес ничто не трогало до экрана выбора игроков. Где в этот адрес записывал единицу уже сам "Конами код". Следовательно просто поменять значение в адресе 002416 не получится. Тогда можно обратить чуть больше внимания на инструкцию:


Address Opcode Mnemonic Arguments A X Y
C2EB A4 24 LDY $0024 02 00 00

Как написано выше, она записывает в регистр Y значение из адреса 002416 и уже ясно, что легко поменять значение этого адреса не получится. Почему бы тогда не поменять значение самого регистра. Для этого нужно поменять опкод A416 на опкод A016 и аргументом передать любое значение отличное от нуля. Сказано — сделано, но тут возникает другая проблема. Открыв "HEX Editor" (из Отладчика этого не сделаешь, придётся воспользоваться меню окна) я не могу поменять этот опкод. Объяснение очень простое, в игре используется так называемый мэппинг. Технология позволяющая запускать на приставке игры большего объёма чем приставка в состоянии принять. Работает он за счёт подмены реальных адресов РОМа на адреса доступные процессору, передавая ему на обработку куски нужные для работы игры прямо сейчас. То есть адрес, что я вижу в отладчике не соответствует реальному адресу на РОМе и я не имею ни малейшего представления где в РОМе искать эту инструкцию. Именно в этот момент выбранный эмулятор начинает сиять, ведь всё что мне сейчас нужно, кликнуть по опкоду правой кнопкой мыши и выбрать пункт "Go Here In ROM File", это перенесёт меня на эту же инструкцию в самом РОМе где я могу смело поменять опкод на новое значение. Ровно таким же образом я поменял аргумент инструкции:


Address Opcode Mnemonic Arguments A X Y
C2EF A9 1D LDA #$1D 02 00 00

С 1D16 на 6316. После обязательно сохраняю результат в новый файл, ведь если я не обошёл все защиты игры я только что сломал РОМ и я не хочу остаться без работающего оригинала.


P.S.: Прошу прощения за многабукафф и тщательное, возможно слишком тщательное пережёвывание. Готов принять любую критику, замечания и исправления. Спасибо вам большое, ваш Виктор.

Теги:
Хабы:
+17
Комментарии19

Публикации

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн