Красным выделены примечания переводчика.
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
Все права на копирование принадлежат ParasyteDragon Eye Studios 2003. Parasyte и Dragon Eye Studios не несут
ответственности за любое использование информации, содержащейся в этом документе. Мы не несём ответственности
за то, что она сожрала вашу домашнюю работу и собаку. Пожалуйста, не забывайте использовать туалетную бумагу.