logo
NEWS ARTiCLES PASSGENS ARCHiVE ABOUT

Документация к FCEUd #2

  1. Разминка
    Перед тем как начать, лучше сразу сказать, что нужно знать, чтобы полностью понять этот документ. Во-первых, нужно знать основы программирования на ассемблере. (Основы просты: нужно понимать каждую команду и режимы адресации команд). Так же нужно быть сведущим в двоичной системе счисления, включая и вычисления в ней. В общем, для того чтобы начать пользоваться FCEUd нужно не так много. Документы по ассемблеру процессоров семейства 6502 и разные доки можно найти на Zophar Domain и ROMHacking.net. Или просто используйте поиск со словами “6502 Assembly” в Гугле. То, что нам понадобится:
    • FCEUd
    • РОМ игры The Addams Family: Pugsley’s Scavenger Hunt,
    • Разные доки, которые можно найти на: www.romhacking.net
  2. Находим пароль
    Первое что надо сделать при вскрытии алгоритмов паролей - это найти введённый пароль в RAM! Для этого можно воспользоваться простым поиском читов. Откроем ром и перейдём на экран ввода пароля. Здесь вводим однин знак “1” (чтобы ввести знак пароля нужно будет нажать А). Затем открываем окно читов в FCEUd через меню NES -> Cheats… С открытым окном читов кликнем Add Cheat(добавить Чит), чтобы открыть Консоль Читов. Здесь вы видите чит сёрч - то, что поможет нам найти введённый пароль в RAM. При начале чит поиска в первую очередь нужно нажать кнопку Reset Search ( Сброс Поиска ). После этого стираем введённую единицу кнопкой B и вводим “2”. Возвращаемся к окну поиска читов и выбираем фильтр “O!=C”. Это означает, что исходное значение ( Original ) не равно текущему ( Current ). Нажмем кнопку “Do search”( произвести поиск ), а потом “Set Original To Current”( установить текущее значение в исходное ), В консоле читов FCEUxd и FCEUXDSP последнее действие проделывается автоматически при нажатии “Do search”. и вернёмся к игре. Теперь вводим три “1”, так что теперь пароль стал “2111”.Вернёмся к читам и поставим фильтр “|O-C|==V2”. Это означает, что разница между исходным и текущимизначениями равна V2. Значение V2 можно вписать в окно слева от фильтров - не пропустишь! В нашем случае V2 должно содержать ноль, потому что первый знак пароля не изменился с момента предыдущего поиска. Нажмём “Do search” ещё раз, потом вернёмся в игру и введём первым знаком пароля “Z”. Теперь нужно будет установить фильтр “O!=C”, так как значение изменилось. Жмем ещё раз на поиск, и так далее пока не остановимся на единственно возможном адресе. Забегая вперёд, могу сказать, что это $043D.

  3. Находим алгоритм проверки пароля
    Итак, теперь когда мы знаем где пароль хранится в RAM, нам нужно использовать эти знания чтобы найти код, отвечающий за проверку пароля. Он должен проверить не является ли введённый пароль случайными знаками!
    Открываем консоль отладки FCEUd через NES -> Debug, или нажав F1. Теперь вернёмся в игру и введём случайный пароль, например “2211B”. Но перед тем как ввести последнюю букву, установим останов на чтение (BPR) из адреса $043D. Исполнение программы тут же останавливается, так что будем работать в области этой команды. Деактивируем BPR перед тем как продолжить.
    С остановленной игрой поставим останов на запись (BPW) в $0441. Это ячейка, в которой содержится наш последний знак пароля. Наконец, вводим последний знак и отладчик остановит выполнение программы из-за второго останова. Его теперь можно удалить - он нам не пригодится. Активируем первый останов и нажмём “Run”! Мы окажемся прямо посреди подпрограммы, отвечающей за расшифровку пароля, с которой и надо разобраться, чтобы написать генератор паролей!
    В окне дизассемблера прокрутим вверх пока не встретим первую команду RTS. К счастью, она в строке прямо над адресом PC. Это означает, что программный счётчик (PC) установлен на начало подпрограммы. Так что можно скопировать весь код, начиная с начала подпрограммы и заканчивая первым RTS который встретится. Просто вставим его в блокнот и реверснём его!

  4. Переводим 6502 в Си или другой язык
    Начинается весёлая часть - непосредственно делаем генератор паролей! Я в первую очередь просто поверхностно просматриваю полученный код. Сразу видны знакомые адреса, которые содержат введённый пароль. Это хорошо.Теперь взглянем на подпрограмму по маленьким частям.

    $BAC8:AD 3D 04 LDA $043D = #$01
    $BACB:18 CLC
    $BACC:6D 3E 04 ADC $043E = #$01
    $BACF:18 CLC
    $BAD0:6D 3F 04 ADC $043F = #$00
    $BAD3:18 CLC 
    $BAD4:6D 40 04 ADC $0440 = #$00 
    $BAD7:18 CLC 
    $BAD8:69 25 ADC #$25 
    $BADA:29 1F AND #$1F 
    $BADC:CD 41 04 CMP $0441 = #$09 
    $BADF:D0 3A BNE $BB1B 

    Очень простой код. Он загружает первый знак пароля из $043D, потом добавляетэто значение к значениям других трёх знаков пароля. Это самая распространённая из всех чексумм, но дальше эта игра делает что-то другое. После создания чексуммы, она прибавляет $25 к этой величине, чтобы немного её изменить. Наконец, она маскирует её $1F (наибольший знак, который может встретиться в пароле). А потом игра просто сравнивает получившуюся чексумму с последним знаком в пароле. Если они совпадают, то она ветвится в $BB1B.
    Проверьте скопированный код, убедитесь, что в нём есть команда по адресу $BB1B. Его может не быть, если вы закончили копировать на первом RTS. $BB1B - очень небольшой кусок кода, который просто возвращает -1, что означает ошибку ( вывод на экран надписи ERROR ).

    $BB1B:A9 FF LDA #$FF 
    $BB1D:60 RTS 

    Только и всего!
    Итак, запомнив всё это, давайте напишем часть программы, выполняющую точно такие же действия как и тот код сверху. Я буду использовать Си, так как это мой любимый язык программирования.

    u8 password[5];
    chksum = ((password[0] + password[1] + password[2] + password[3] + 0x25) & 0x1F);
    if (password[4] != chksum) return -1;                                       

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

    $BAE1:AD 3D 04 LDA $043D = #$01 
    $BAE4:49 0A EOR #$0A 
    $BAE6:0A ASL
    $BAE7:0A ASL 
    $BAE8:0A ASL 
    $BAE9:0A ASL 
    $BAEA:8D 3B 04 STA $043B = #$00 
    $BAED:AD 3E 04 LDA $043E = #$01 
    $BAF0:49 0F EOR #$0F 
    $BAF2:4A LSR 
    $BAF3:0D 3B 04 ORA $043B = #$00 
    $BAF6:8D 3B 04 STA $043B = #$00 
    $BAF9:29 02 AND #$02 
    $BAFB:D0 1E BNE $BB1B 

    Загружается первый знак пароля, затем он XOR’ится с $0A, и четыре раза логически сдвигается влево. Затем он временно сохраняется, а загружается второй знак пароля, который XOR’ится c $0F. Наконец, значение сдвигается вправо один раз, после чего оно OR’ится с предыдущим полученным занчением. Кроме того, есть небольшая проверка на то выставлен ли первый бит. Если бит равен единице, то подпрограмма возвращает ошибку ( зачем нужна эта проверка - см. ниже ).

    u8 decode[3]; //buffer
    decode[2] = (((password[0] ^ 0x0A) << 4) | ((password[1] ^ 0x0F) >> 1));
    if (decode[2] & 0x02) return 1;                                       

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

    $BAFD:AD 3F 04 LDA $043F = #$00 
    $BB00:49 05 EOR #$05 
    $BB02:8D 37 04 STA $0437 = #$00 

    Загружается третий знак, XOR’ится с $05, сохраняется в буффер!

    decode[1] = (password[2] ^ 0x05);                         

    Видите?! Так просто!!

    $BB05:AD 40 04 LDA $0440 = #$00 
    $BB08:49 02 EOR #$02 
    $BB0A:4A LSR 
    $BB0B:8D 38 04 STA $0438 = #$05 

    Загружает четвёртый знак, XOR’ит с $02, сдвигает один раз вправо и сохраняет в буффер!

    decode[0] = ((password[3] ^ 0x02) >> 1);                         

    Остаток алгоритма по большей части бесполезен, так что его можно смело проигнорировать. Всё что он делает, так это просто заполняет какие-то ячейки памяти.

  5. Собираем всё вместе
    Всё что осталось сделать, это собрать всё это вместе в одну программу и у нас будет полнофункциональный контролёр паролей. Ну это, конечно, здорово, но ведь нам нужна программа, которая генерирует пароли, а не проверяет их! Как наша задача соотносится с тем что мы написали? В 9 из 10 случаев, ответ будет “РЕВЕРСИРОВАТЬ!” Да, точно: возьмите программу и перепишите её с точностью до наоборот. Например, вот полная функция проверки пароля.
    Надеюсь, вы поняли как я перевернул программу. Всё нужно делать наоборот: например, вместо прибавить нужно вычитать. Обратите внимание на маску, которую я сделал для password[1]. Эти команды просто предотвращают превышение максимальновозможного занчения знака пароля ($1F). В большинстве случаев применять такие сложные действия проделывать и не придётся: куда легче будет дойти до того места, где происходит генерирование пароля самой игрой и немного подсмотреть алгоритм ;)
    Теперь просто соберём это всё в милую программку и готово!

  6. Понимание Данных
    Секрет изготовления своего генератора паролей в понимании данных, изымаемых из знаков пароля. Я просто сравнивал данные буфферов (buffer) с предметами, которые мне давали. Вкратце: buffer[0] - десятки жизней героя, buffer[1] - единицы жизней героя и, наконец, в buffer[2] хранятся данные о числе сердец и спасённых членах семьи.
    Эта секция обычно доставляет наибольшие проблемы начинающим. Ну с жизнями ещё куда не шло: можно скомпилировать генератор, подставить в значения буферов какие-то числа и посмотреть результат. Жизни видно сразу без напряжения. А вот со вторым буфером я в свое время так и не разобрался: исписал два листа вдоль и поперёк всевозможными комбинациями, но так и не понял принципа. Всё дело в “битовой логике”. Логические операции с битами очень часто используются для хранения данных в пароле. Ну, конечно, далеко не только для этого (взгляните в любое место кода любой игры на NES (да вообще, в любой ассемблерный листинг) - везде логические операции). Это основы, которыми просто необходимо овладеть. Для тех кто до сих пор не подозревал о существовании логических операций рекомендуется почитать вот эту записку. В дальнейшем будем исследовать ячейку, содержащую decode[2] ($043B). Поставим останов на чтение из ячейки, обозначенной нами как decode[2], и посмотрим как же её используют после формирования из данных пароля.

    $C6F6:AD 3B 04 LDA $043B = #$04  
    $C6F9:A2 02 LDX #$02(две жизни есть всегда!) 
    $C6FB:0A ASL 
    $C6FC:90 01 BCC $C6FF 
    $C6FE:E8 INX 
    $C6FF:0A ASL 
    $C700:90 01 BCC $C703 
    $C702:E8 INX 
    $C703:0A ASL 
    $C704:90 01 BCC $C707 
    $C706:E8 INX 
    $C707:8E 36 04 STX $0436 = #$03; число возможных жизней(доступны, но не заполнены) 
    $C70A:8E 35 04 STX $0435 = #$02; число заполненных жизней 

    Как видите, заполненные жизни всегда будут равны возможным, потому что запись восстанавливает жизни. Судя по коду, наибольшее возможное число - 5, скажем, если decode[2] равен $E0. Так и есть - в игре максимум пять сердец. Почему-то при загрузке уровня произошло только такое использование этого байта. Куда же ушли остальные (пока не использованные) пять бит?! Может из ячейки будут читать, когда уже надо выводить информацию о тех, кого спас герой, т.е. при нажатии на паузу? Останов произошел по адресу B8С4, однако там страшная процедура, разобраться в которой под силу только далеко не начинающему хакеру. А так как мы не из таких, то легче, а самое главное, быстрее будет просто подставлять нужные нам неизвестные пять бит в память приставки и посмотрим что будет. Подставляем и нажимаем на паузу - данные пересчитываются.
    Итак:
    младший бит: Гомез
    бит 1: Мортиша (см. ниже)
    бит 2: Дядя Фестер
    бит 3: Бабуля
    бит 4: Венздей
    Остаётся вопрос: почему же игра так упорно не желает, чтобы пароль хранил информацию о спасении Мортиши? Посмотрите на команду

    $BAF9:29 02 AND #$02
    $BAFB:D0 1E BNE $BB1B  

    Проверяет спасена ли Мортиша и если да, то выводит сообщение о неправильном пароле - пароля со спасенной женой Гомеза не должно существовать. Те кто проходил игру знают, что она нелинейна: родственников можно спасать практически в любом порядке, однако последней всегда должна быть Мортиша, т.к. дверь, за которой она заточена, и где скрывается финальный босс - Судья, заперта до тех пор, пока герой не спасёт всех остальных. Нам повезло, что информация о героях считывается во время паузы, а не во время анализа пароля, потому что если бы мы вписывали значения до принятия пароля, то он бы просто не был принят (разумеется, если мы не поменяем код ;))
    Для тех, кому интересно “а что будет, если…” могу сообщить: если ввести пароль, в котором все родственники, включая Мортишу будут спасены и вновь придти к Судье, то можно будет наблюдать такую безобразную картину:
    addamspwd.png
    Судья неподвижен, и кроме него и Пагсли ничего больше не загружается. Более того, очевидно, код, который проверяет спасена ли Мортиша используется прямо во время поединка, так что подтасовывая байты в ячейке можно периодически заставлятьсудью вставать, а уровень - становиться опустошённым. А что касается того пароля, который выдаётся по окончании игры, то это обычный пароль с четырьмя спасёнными членами семьи (Мортиша опять в заточении), и не понятно зачем он вообще нужен.