МЕНЮ
Новости
Переводы
Проекты
Утилиты
Прохождения
Документация
Разное
БАННЕРЫ
Наш баннер
Новая реальность
Emu-Land
RSP - проект по русификации приложений для Symbian
SkatePark
Игры и мультики на FinikPro.narod.ru!
Сайт о легендарной онлайн игре Ultima Online
Langrisser
Сайт об играх от Technos Japan
HotLog
 

 

 

 

 

 

FCEUd - Генераторы паролей
Красным выделены примечания переводчика.

-===========---

1) Разминка

-===============---

 

   Перед тем как начать, лучше сразу сказать, что нужно знать, чтобы полностью понять этот документ. Во-первых, нужно знать основы программирования на ассемблере. (Основы просты: нужно понимать каждую команду и режимы адресации команд). Так же нужно быть сведущим в двоичной системе счисления, включая и вычисления в ней. В общем, для того чтобы начать пользоваться FCEUd нужно не так много. Документы по ассемблеру процессоров семейства 6502 и разные доки можно найти на Zophar Domain  и ROMHacking.net. Или просто используйте поиск со словами "6502 Assembly"  в Гугле.

То, что нам понадобится:

FCEUd,

РОМ игры The Addams Family: Pugsley's Scavenger Hunt,

Разные доки, которые можно найти на сайтах:

   http://www.zophar.net/

   http://www.obelisk.demon.co.uk/6502/reference.htm

   http://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 случаев, ответ будет "РЕВЕРСИРОВАТЬ!" Да, точно: возьмите программу и перепишите её с точностью до наоборот. Например, вот полная функция проверки пароля.

   

u8 VerifyPass(u8 *password, u8 *buffer) {

   int chksum = ((password[0]+password[1]+password[2]+password[3]+0x25)&0x1F);

   if (password[4] != chksum) return -1:

   buffer[2] = (((password[0]^0x0A)<<4)|((password[1]^0x0F)>>1));

   if (buffer[2]&0x02) return -1;

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

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

   return 0;

}

 

   Реверсируем все операции программы.

 

u8 GeneratePass(u8 *password, u8 *buffer) {

   if (buffer[2]&0x02) return -1;

   password[0] = ((buffer[2]>>4)^0x0A);

   password[1] = (((buffer[2]<<1)^0x0F)&0x1F);

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

   password[3] = ((buffer[0]<<1)^0x02);

   password[4] = ((password[0]+password[1]+password[2]+password[3]+0x25)&0x1F);

   return 0;

}

 

   Надеюсь, вы поняли, как я перевернул программу. Всё нужно делать наоборот: например, вместо прибавления нужно делать вычитание. Обратите внимание на маску, которую я сделал для 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

 

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

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

 


Судья неподвижен, и кроме него и Пагсли ничего больше не загружается. Более того, очевидно, код, который проверяет спасена ли Мортиша используется прямо во время поединка, так что подтасовывая байты в ячейке можно периодически заставлять судью вставать, а уровень - становиться опустошённым. А что касается того пароля, который выдаётся по окончании игры, то это обычный пароль с четырьмя спасёнными членами семьи (Мортиша опять в заточении), и не понятно зачем он вообще нужен.


-===============---

  7) Legal Information

-===============---

 

    Все права на копирование принадлежат Parasyte\Dragon Eye Studios 2003. Parasyte и Dragon Eye Studios не несут ответственности за любое использование информации, содержащейся в этом документе. Мы не несём ответственности за то, что она сожрала вашу домашнюю работу и собаку. Пожалуйста, не забывайте использовать туалетную бумагу.

Перевод: Griever