$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