"Экстремальный ромхакинг для "чайников" Этот документ написан человеком, который совсем недавно узнал "шо такое дебугер":), оценил его возможности и впервые самостоятельно с помощью дебага "сломал" алгоритм паковки. То есть мной=) Поэтому мануал должен быть понятен любому ромхакеру, уже поднаторевшему в базовом ромхакинге, но сомневающемуся в своих силах осилить более высокую ступень ромхакинга - экстремальный. В качестве примера я буду рассматривать платформу Super Nintendo, с которой я и начинал. Сначала скажу, что потребуется для успешного взлома (кроме стандартных ромхакерских утилит вроде Translhextion). Во-первых, это лучший (прим. автора - на мой взгляд) отладчик для SNES на основе эмулятора Snes9X - Geiger’s Snes9x Debugger Mark 9. Во-вторых, любой референс-документ по ассемблеру для процессора 65c816 (SNES). В-третьих, будет полезна утилита Lunar Address для конвертирования PC-адресов в формат SNES Lo/HiROM и обратно. Найти всё это добро можно на http://www.romhacking.net. Ну и в-четвёртых, ваши главные инструменты - прямые руки и трезвая голова;) Я не буду описывать основы архитектуры платформы SNES. Предполагается, что вы самостоятельно ознакомитесь с ними по многочисленным докам. Также я подразумеваю, что вам известны такие понятия как "регистр", "ячейка памяти","команды ветвления", "дизасемблирование" и т. п. Описывать ход процесса "обратной разработки" или просто взлома алгоритма сжатия я буду на примере игры Shodai Nekketsu Kouha Kunio-kun. Японские разработчики - народ мутный, и иногда сложно понять мотивацию их действий:) Эта игра не исключение. В ней не сжаты ни текст, ни шрифт, ни графика, зато пожато большинство тайловых карт. В первую очередь, это тайловая карта титульного экрана с японским названием игры. Надпись с копирайтом внизу не сжата, но остальное лого запакованно довольно мудрёным алгоритмом (прим. автора - опять же, это моё мнение как новичка, для "набивших руку" ромхакеров-экстремалов этот алгоритм может показаться проще пареной репы). Вот его-то и будем ломать. Итак, запустите отладчик и откройте игру. Заметили разницу между обычным эмулятором и этим? При запуске выскакивает окошко отладчика (Debug Console) с первой выполняемой командой. Нам надо определить тот момент времени, когда в память (RAM) записывается уже распакованная тайловая карта для лого. Поэтому нажимаем кнопку "Run" и ждём появления экрана с названием игры. Некоторые игры слишком быстро затирают информацию в памяти, поэтому иногда бывает достаточно сложно поймать тот момент, когда необходимые нам данные находятся в памяти. Впрочем, к этой игре это не относится. Поэтому когда появился титульник, не спеша нажимаем на окошко Debug Console (игра при этом приостанавливается). Теперь нам надо как-то найти в памяти распакованную тайловую карту (далее - ТК). Есть два способа. Первый - универсальный, подходит для любых данных (текст, графика и т. п.). Нужно сделать сейв, и в нём с помошью хекс-редактора обычными базовыми методами найти нужные данные (учтите только, что сейвы этого эмулятора дополнительно пакуются, распаковать их можно любым архиватором вроде WinRAR или WinZIP). Кроме того, к адресу найденного ресурса в сейве необходимо прибавить дополнительное смещение, для сейвов этого эмулятора смещение равно[]. Второй способ более практичен для нашего случая. В тайловом редакторе нам надо просто отыскать первый тайл в картинке и узнать его адрес. Теперь, независимо от выбраного вами способа, проделайте следующее. Перезапустите игру и дождитесь момента, когда потухнет экран с логотипом Technos Japan. Поставьте паузу (File->Pause) и вернитесь в окно отладчика. Нажмите кнопку "Breakpoints". Это т. н. точки останова, то есть адреса, при обращении к которым игра приостановится и будет вызван отладчик. Нам известен адрес ресурса, вписываем его в верхнее поле (предварительно конвертировав в SNES-формат). Для второго способа это будет адрес D47E0h (1AC7E0 для SNES, его и надо вписывать). Поставьте галочку "Read" и нажмите OK. Уберите паузу и продолжите выполнение игры кнопкой "Run". Игра практически сразу приостановится и будет выведена команда, которая обращалась к нашему адресу. Это и есть наш алгоритм распаковки (правда, он пойдёт немного дальше). Забегая вперёд, скажу, что сжатая ТК хранится в роме по адресу 45D73h ($08:DD73 для SNES). Теперь предстоит один из самых скучных этапов. Нам надо проследить алгоритм распаковки как можно детальнее, выяснив все переходы в процедуре распакови ТК (переходы - это все команды типа BRA, BPL, BEQ, JMP и т. п., формат зафисит от платформы, информацию о них можно получить в документации по ассемблеру данной платформы) и определить её границы. Для этого нажимайте кнопку "Step Over", пока все появишиеся переходы не будут указывать на уже выводившиеся адреса. Желательно после этого скопировать диззасемлированный код в текстовый файл и сохранить для последующего изучения. Для упрощения поиска всех ссылок-переходов переходы, которые остались неопределёнными, можно продезассемблировать отдельно. Для этого очистите текст в выводе (кнопкой "Clear Text"), затем нажмите кнопку "Disassemble". Из сохранённого кода возьмите адрес неизвестного перехода (например, 88DF для нашего случая) и впишите его в поле "Start Address". Проконтролируйте значение поля "End Address", оно должно быть больше примерно на 50-100h (здесь лучше не экономить). Не трогайте галочки в этом окне и нажмите "ОК". Сохраните дизассемблированный код и выясните, нет ли в нём ещё каких-нибудь переходов. Повторяйте эту процедуру, пока не определите чёткие границы процедуры распаковки (обычно командой выхода из процедуры является RTS, если вы нашли её в коде, значит, вы на правильном пути). В результате у вас должно получиться вот это: Disassembly 885D_8917 (tilemap_unpack_to_RAM): $00/885D AD 0F 09 LDA $090F [$00:090F] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8860 F0 06 BEQ $06 [$8868] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8862 AE 15 09 LDX $0915 [$00:0915] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8865 E8 INX A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8866 80 1C BRA $1C [$8884] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8868 C2 20 REP #$20 A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/886A AD 15 09 LDA $0915 [$00:0915] A:0000 X:0009 Y:FAF9 P:eNvmxdizC $00/886D 8D 13 09 STA $0913 [$00:0913] A:0000 X:0009 Y:FAF9 P:eNvmxdizC $00/8870 18 CLC A:0000 X:0009 Y:FAF9 P:eNvmxdizC $00/8871 69 00 08 ADC #$0800 A:0000 X:0009 Y:FAF9 P:eNvmxdizC $00/8874 8D 15 09 STA $0915 [$00:0915] A:0000 X:0009 Y:FAF9 P:eNvmxdizC $00/8877 E2 30 SEP #$30 A:0000 X:0009 Y:FAF9 P:eNvmxdizC $00/8879 60 RTS A:0000 X:0009 Y:FAF9 P:eNvMXdizC $00/887A E2 20 SEP #$20 A:0000 X:0009 Y:FAF9 P:eNvMXdizC $00/887C C2 10 REP #$10 A:0000 X:0009 Y:FAF9 P:eNvMXdizC $00/887E AE 15 09 LDX $0915 [$00:0915] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8881 A0 00 00 LDY #$0000 A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8884 B7 36 LDA [$36],y[$55:504E] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8886 C8 INY A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8887 8D 0F 09 STA $090F [$00:090F] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/888A 29 C0 AND #$C0 A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/888C F0 CF BEQ $CF [$885D] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/888E C9 C0 CMP #$C0 A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8890 F0 4D BEQ $4D [$88DF] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8892 C9 80 CMP #$80 A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8894 F0 18 BEQ $18 [$88AE] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8896 AD 0F 09 LDA $090F [$00:090F] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8899 29 3F AND #$3F A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/889B 8D 10 09 STA $0910 [$00:0910] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/889E B7 36 LDA [$36],y[$55:504E] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88A0 C8 INY A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88A1 9F 00 00 7F STA $7F0000,x[$7F:0009] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88A5 E8 INX A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88A6 E8 INX A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88A7 CE 10 09 DEC $0910 [$00:0910] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88AA 10 F5 BPL $F5 [$88A1] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88AC 80 D6 BRA $D6 [$8884] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88AE AD 0F 09 LDA $090F [$00:090F] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88B1 29 1F AND #$1F A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88B3 8D 10 09 STA $0910 [$00:0910] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88B6 AD 0F 09 LDA $090F [$00:090F] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88B9 29 20 AND #$20 A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88BB D0 11 BNE $11 [$88CE] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88BD B7 36 LDA [$36],y[$55:504E] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88BF C8 INY A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88C0 9F 00 00 7F STA $7F0000,x[$7F:0009] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88C4 E8 INX A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88C5 E8 INX A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88C6 1A INC A A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88C7 CE 10 09 DEC $0910 [$00:0910] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88CA 10 F4 BPL $F4 [$88C0] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88CC 80 B6 BRA $B6 [$8884] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88CE B7 36 LDA [$36],y[$55:504E] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88D0 C8 INY A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88D1 9F 00 00 7F STA $7F0000,x[$7F:0009] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88D5 E8 INX A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88D6 E8 INX A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88D7 3A DEC A A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88D8 CE 10 09 DEC $0910 [$00:0910] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88DB 10 F4 BPL $F4 [$88D1] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88DD 80 A5 BRA $A5 [$8884] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88DF AD 0F 09 LDA $090F [$00:090F] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88E2 29 0F AND #$0F A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88E4 8D 11 09 STA $0911 [$00:0911] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88E7 9C 10 09 STZ $0910 [$00:0910] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88EA AD 0F 09 LDA $090F [$00:090F] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88ED 29 30 AND #$30 A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88EF D0 06 BNE $06 [$88F7] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88F1 B7 36 LDA [$36],y[$55:504E] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88F3 C8 INY A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88F4 8D 10 09 STA $0910 [$00:0910] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88F7 8C 13 09 STY $0913 [$00:0913] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88FA AD 11 09 LDA $0911 [$00:0911] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/88FD 8D 12 09 STA $0912 [$00:0912] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8900 AC 13 09 LDY $0913 [$00:0913] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8903 B7 36 LDA [$36],y[$55:504E] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8905 C8 INY A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8906 9F 00 00 7F STA $7F0000,x[$7F:0009] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/890A E8 INX A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/890B E8 INX A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/890C CE 12 09 DEC $0912 [$00:0912] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/890F 10 F2 BPL $F2 [$8903] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8911 CE 10 09 DEC $0910 [$00:0910] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8914 10 E4 BPL $E4 [$88FA] A:0000 X:0009 Y:FAF9 P:eNvMxdizC $00/8916 4C 84 88 JMP $8884 [$00:8884] A:0000 X:0009 Y:FAF9 P:eNvMxdizC Видите? У нас есть команда RTS и команда JMP (здесь она осуществляет переход на адрес $8884). $8884 - это основная подпрограмма в процедуре. Изучая код, можно заметить, что к ней часто идёт обращение. И не один переход не осуществляется за пределы этого кода - то есть точно определены границы процедуры распаковки. Для наглядности я дополнительно разделил весь код пробелами, указывающими на начало очередной подпрограммы в процедуре (т. е. для каждого перехода). Осталось правильно переписать этот код на языке высокого уровня. Я немного знаю языки Basic:) и C, но гораздо лучше знаком с языком Pascal (среда Delphi), поэтому привожу пример на нём. Здесь тоже стоит оговориться. Я переписываю полученный код напрямую, "в лоб", т. е. как бы представляю ассемблерный снесовский код на языке высокого уровня. При этом я совсем не оптимизирую его и не стараюсь заменить команды на более простые конструкции, принятые в языках высокого уровня. Поэтому код получается очень грубый и с лишними операторами/переменными, но зато максимально точный. Вот что получилось у меня после переписывания приведённого выше ассемблерного кода на языке высокого уровня: //A,X,Y - регистры //ZF,CF,NF - флаги в регистре состояния //PAKfile - исходный файл (сжатая тайловая карта) //UNPAKfile - выходной файл (распакованная тайловая карта) ZF:=0; CF:=0; NF:=0; A:=0; X:=0; Y:=0; //начало распаковки goto M8884; M885D: A:=h090F; if ZF=1 then goto M8868; X:=h0915; inc(X); goto M8884; M8868: ZF:=0; A:=h0915; h0913:=A; CF:=0; A:=A+$0800; goto theend;//$8879 RTS - возвращение из подпрограммы распаковки X:=h0915; //h0915 - в Х Y:=0; //Обнуление Y M8884: BlockRead(PAKfile,A,1);//байт из сжатой карты в A inc(Y); h090F:=A;//записать A в h090F A:=A and $C0; if A=0 then ZF:=1 else ZF:=0; if ZF=1 then goto M885D; //CMP #$C0 if A=$C0 then goto M88DF; //CMP #$80 if A=$80 then goto M88AE; A:=h090F; A:=A and $3F; if A=0 then ZF:=1 else ZF:=0; h0910:=A; BlockRead(PAKfile,A,1); inc(Y); M88A1: h7F0000:=A; BlockWrite(UNPAKfile,h7F0000,1);//запись байта в распакованную карту inc(X); inc(X); dec(h0910); if h0910>=0 then goto M88A1; goto M8884; M88AE: A:=h090F; A:=A and $1F; if A=0 then ZF:=1 else ZF:=0; h0910:=A; A:=h090F; A:=A and $20; if A=0 then ZF:=1 else ZF:=0; if ZF=0 then goto M88CE; BlockRead(PAKfile,A,1); inc(Y); M88C0: h7F0000:=A; BlockWrite(UNPAKfile,h7F0000,1); inc(X); inc(X); inc(A); dec(h0910); if h0910>=0 then goto M88C0; goto M8884; M88CE: BlockRead(PAKfile,A,1); inc(Y); M88D1: h7F0000:=A; BlockWrite(UNPAKfile,h7F0000,1); inc(X); inc(X); dec(h0910); if h0910>=0 then goto M88D1; goto M8884; M88DF: A:=h090F; A:=A and $0F; h0911:=A; h0910:=0; A:=h090F; A:=A and $30; if A=0 then ZF:=1 else ZF:=0; if ZF=0 then goto M88F7; BlockRead(PAKfile,A,1); inc(Y); h0910:=A; M88F7: h0913:=Y; M88FA: A:=h0911; h0912:=A; Y:=h0913; M8903: BlockRead(PAKfile,A,1); inc(Y); h7F0000:=A; BlockWrite(UNPAKfile,h7F0000,1); inc(X); inc(X); dec(h0912); if h0912>=0 then goto M8903; dec(h0910); if h0910>=0 then goto M88FA; goto M8884; theend: Дальше надо просто оформить этот код в простую программу, которая позволяет открывать бинарный файл со сжатой ТК, распаковывать её и сохранять в выходном бинарном файле. ОЧЕНЬ ВАЖНО! Следите за состоянием регистров и флагов: переходы осуществляются в основном по состоянию флагов, а в языке высокого уровня не всегда можно найти простую замену такой проверки. Поэтому необходимо ставить дополнительные условия (в данном случае я проверял состояние аккумулятора (А) и ставил дополнительное условие при его обнулении (if A=0 then ZF:=1 else ZF:=0; )). После написания программы, возьмите получившийся выходной файл с расжатой ТК и посмотрите его в хекс-редакторе. Если вы использовали первый способ нахождения данных, то вам уже известен вид распакованной ТК в памяти. Если нет, то запустите игру в откладчике и приостановите игру на логотипе с названием. В отладчике нажмие кнопку "Show Hex", выберите участок ОЗУ - RAM и выставите границы от 7F0800h до 7FFFFFh. Это распакованная ТК в оригинале. То, что вы видите здесь, должно быть идентично полученному выходному файлу вашей программы-распаковщика. Единственное, для нашего случая в память распакованная ТК пишется через байт, на тот участок, в котором уже имеется предыдущий код. Здесь не будет полной идентичности (байт в байт), но распакованная нашей программой ТК всё равно корректна. Вот и всё, собственно. Успешного вам экстрим-ромхакинга!