[Из песочницы] Кидхак Prehistorik 2: анлочим уровни
Любителям этой замечательной игры посвящается… … Если таковые еще есть. Впрочем, я уверен, что не все так плохо и Crysis не поглотил мозг человечества целиком и окончательно.Так вот, вышеупомянутые любители старых игр и в частности одной замечательной игры 92 года могут помнить, что система сохранений в игре сделана в несколько приставочном духе: по уровням разбросаны (иногда в довольно неожиданных и труднодоступных местах) коды, которые во времена MS-DOS старательно записывались на бумажечку и хранились, как сокровище. Код, введенный в главном меню, позволял начать соответствующий уровень со стартовой позиции.Soshite, в наше время появилась, в некотором роде, проблема для тех, кто хотел бы поиграть в Prehistorik 2. А именно — необходимость эмулировать игру в Dosbox: далеко не у всех есть возможность найти на свалке или, что еще гораздо реже, вытащить с антресолей заботливо хранящийся там 486-й.Дело в том, что при генерации кодов игра использует переменные окружения BIOS. Как это эмулируется в Dosbox, я не разбирался, знаю достоверно, что при каждом новом запуске эмулятора коды в игре другие. Соответственно, бумажка с кодами становится бесполезной, а возможность приятного и ненапрягающего прохождения игры сводится практически на нет: полностью игру можно пройти только за один присест. Возможно, это решается детальной настройкой Dosbox. Но… это так тривиально, настолько тривиально, что пугает даже больше, чем небольшое копание в исполняемом файле игры.Постановка задачи Реализовать небольшой хак, в результате которого олдгеймер вроде меня мог бы вместо кода ввести просто номер желаемого уровня. Перейти сразу к результатам.Решение Больше всего времени у меня отняла распаковка файла игры. К изначально запакованному коду игры было добавлено интро команды, взломавшей когда-то защиту от копирования, и затем все это было снова запаковано другим алгоритмом. За помощью в распаковке я обратился к статье г-на Гречникова: habrahabr.ru/post/187072/, в которой меня заинтересовал раздел под заголовком «Распаковка», а конкретно следующий абзац:
То есть, остаток процедуры loc_19960 не вызывается, если передать в командной строке параметр /ni. Пробуем, видим, что с этим параметром игра пропускает напоминание о доблестной команде Hybrid и сразу переходит к собственно игре. Делаем вывод, что весь seg002 — ненужная для игры заставка, навешенная на рабочий exe’шник. Переписываем в заголовке cs: ip на 0000:0003, ss: sp на 1681:0080, обрезаем последние 5E90h байт, соответственно скорректировав поля размера и дополнительной памяти в заголовке.
(Дальше было неинтересно, т.к. готовое решение — это не то, чего мне хотелось.)После того, как я последовал данному совету, я обнаружил яйцо в утке, извлеченной ранее из зайца… или как там в старинных русских сказках обычно бывало? Проще сравнить это с матрешкой:
После распаковки diet программа выросла до 90 Кб и уже ничто не мешало запустить в нее грязные лапы. Чем я и занялся, открыв Hiew и сразу же найдя интересующую меня строку:
Уточню для таких же, как я, набирающихся опыта (хотя учиться стоит всегда), что поиск строки в Hiew — это сразу после запуска F4 — Hex, F7 — ASCII → «Интересующая нас строка».Адрес строки — 15401. Теперь мне нужно узнать, в каком месте программы происходит обращение к этой строке. Естественно, что такие обращения с высокой долей вероятности будут близки к той части, которую я вознамерился ломать.Как это можно узнать? Путей, на самом деле, множество. Можно открыть файл в Ida Pro и изучить перекрестные ссылки. Правда, вначале настроив Ida так, чтобы она дружила с используемым типом строк. Но поскольку Id’у я не смог подружить с Dosbox’ом на таком уровне, чтобы можно было спокойно и без раздражающих факторов отлаживать 16-битные файлы (Dosbox постоянно забывал, что он отладчик и пытался жить своей жизнью, несмотря на патч и плагин), то я решил вообще не использовать такую тяжелую артиллерию там, где нужно, скорее всего, просто подправить несколько байт.Поэтому поступим проще. Чтобы выяснить, где используется смещение на эту строку в программе, нужно сначала выяснить само смещение. Для этого нужно узнать базу сегмента данных. В Hiew это более чем просто. Жмем F8 и смотрим заголовок файла.
Итак, для начала откроем калькулятор, переключимся в шестнадцатиричную систему и введем в него значение Paragraphs in header: 86, после чего умножим на 10. Скопируем это значение в буфер и вернемся в Hiew, где все еще не закрыто окошко заголовка. Не выходя, нажмем F5 для перехода в точку входа. И вот тут уже перейдем в режиме decode (F4 — decode). Перед нами такой листинг:
листинг 1.
00000468: FA cli 00000469: FC cld 0000046A: BAFFFF mov dx,0FFFF;» » 0000046D: BE8000 mov si,00080;» А» 00000470: AC lodsb 00000471: 98 cbw 00000472: 8BC8 mov cx, ax 00000474: E35E jcxz 0000004D4 ---↓ (1) 00000476: AC lodsb 00000477: 3C2F cmp al,02F;»/» 00000479: 7404 je 00000047F ---↓ (2) 0000047B: E2F9 loop 000000476 ---↑ (3) 0000047D: EB55 jmps 0000004D4 ---↓ (4) 0000047F: AC lodsb 00000480: 24DF and al,0DF;»▀» 00000482: 3C46 cmp al,046; «F» 00000484: 7506 jne 00000048C ---↓ (5) 00000486: 2E83260300FE and w, cs:[0003],0FFFE;»■» 0000048C: 3C4D cmp al,04D; «M» Все это нам неинтересно. Нам интересно присвоение значения регистру ds. Даже не прибегая к поиску, мы прокручиваем экран совсем немного вниз и находим искомое:
листинг 2.
000004D4: B8030A mov ax,00A03;»◙♥» 000004D7: 8ED8 mov ds, ax Вытаскиваем 00А03 и запихиваем в калькулятор, где тоже умножаем на 10 и прибавляем к сохраненному ранее в буфере значению. Получаем А290. А теперь вычтем А290 из 15401: В171. Это смещение строки, которое будет использоваться внутри программы. Теперь переходим в гекс-отображение (F4 — Hex) и жмем F7, куда в строку, поименованную басурманским словом «Нех», введем 71 В1 (байты идут в обратном порядке, помним ведь?). Выходим на следующий листинг:
листинг 3.
00009E23: E85801 call 000009F7E ---↓ (1) 00009E26: C606A7B103 mov b,[0B1A7],003;»♥» 00009E2B: BB71B1 mov bx,0B171;»▒q» 00009E2E: C706A2B1F20A mov w,[0B1A2],00AF2;»◙Є» 00009E34: E8EDFE call 000009D24 ---↑ (2) 00009E37: C606A7B104 mov b,[0B1A7],004;»♦» 00009E3C: BB6CB1 mov bx,0B16C;»▒l» 00009E3F: C706A2B1C912 mov w,[0B1A2],012C9;»↕╔» 00009E45: E8DCFE call 000009D24 ---↑ (3) 00009E48: 803EA6B101 cmp b,[0B1A6],001;»☺» 00009E4D: 7503 jne 000009E52 ---↓ (4) 00009E4F: E90001 jmp 000009F52 ---↓ (5) 00009E52: 803EA6B102 cmp b,[0B1A6],002;»☻» 00009E57: 7503 jne 000009E5C ---↓ (6) 00009E59: E90601 jmp 000009F62 ---↓ (7) 00009E5C: 8A1E7028 mov bl,[2870] 00009E60: F6C380 test bl,-080; «А» 00009E63: 7403 je 000009E68 ---↓ (8) 00009E65: E91501 jmp 000009F7D ---↓ (9) 00009E68: A02928 mov al,[2829] 00009E6B: 0A060C28 or al,[280C] В начале этого листинга происходят действия с интересующей нас строкой, а затем те же действия — с соседствующей строкой »[[[[». Пожалуй, статическим наблюдением дальше не обойдешься. Нужно прибегнуть к помощи отладчика. Чтобы не нарушать атмосферу консольности, прибегну к специальному выпуску Dosboх 0.74, содержащему в себе дебаггер. Кроме того, этот вариант гораздо стабильнее, чем костыль Ida + Dosbox plugin.
Итак, открываю Dosbox-74-debug (предварительно настроив conf-файл, где просто указаны команды монтирования директории игры и директивы ее запуска с префиксом debug). Не мудрствуя лукаво, запускаем игру в режиме трассировки и вскоре узнаем, что вот в этом месте:
листинг 4.
022E:014C 7B00 jpo 0000014E ($+0) 022E:014E C606A46C00 mov byte [6CA4],00 022E:0153 B80300 mov ax,0003 022E:0156 E86701 call 000002C0 ($+167) 022E:0159 E8138D call FFFF8E6F ($-72ed) 022E:015C 803E862D08 cmp byte [2D86],08 022E:0161 7234 jc 00000197 ($+34) После вызова второй подфункции запускается главное меню, которое «крутится» внутри этой подфункции. Ставим бряк на входе в первую подфункцию и запускаем игру заново, отправляя поначалу в свободный полет.Вскоре выясняется, что…
… отрисовка главного меню заканчивается на этой команде. А сразу за ней следует такой листинг:
листинг 5.
022E:8E99 8ED8 mov ds, ax 022E:8E9B 33C0 xor ax, ax 022E:8E9D A3EC27 mov [27EC], ax 022E:8EA0 A3EE27 mov [27EE], ax 022E:8EA3 32C0 xor al, al 022E:8EA5 3806F227 cmp [27F2], al 022E:8EA9 7548 jne 00008EF3 ($+48) 022E:8EAB 3806F327 cmp [27F3], al 022E:8EAF 7557 jne 00008F08 ($+57) 022E:8EB1 A02928 mov al,[2829] 022E:8EB4 0A060C28 or al,[280C] 022E:8EB8 7539 jne 00008EF3 ($+39) 022E:8EBA 813EEC270E01 cmp word [27EC],010E 022E:8EC0 72E3 jc 00008EA5 ($-1d) Главное меню, напомню, предлагает нажать либо 1, либо 2. В листинге два перехода. В этом месте игра зацикливается между 022E:8EA5 и 022E:8EC0. Все очевидно. Установим брейкпоинты на оба варианта: ВР 022Е:8EF3 и ВР 022Е:8F08, «отпустим» игру в свободный режим, нажав F5 и нажмем 2 — пункт меню, предлагающий ввести password.
Спустя некоторое время трассировки приходим к листингу 3. Строка »[[[[», отрисовывающаяся вместе с«ENTER CODE» — может быть либо строкой »_ _ _ _», которая отображается таким образом из-за хитрого шрифта, либо зарезервированным местом под код. Допустим, что верно последнее и установим брейкпойнт на обращение к памяти по смещениям подозрительных 4 байт: ВРМ 0С11: В16С, ВРМ 0С11: В16D, ВРМ 0С11: В16E и ВРМ 0С11: В16F (текущую базу сегмента данных видим в отладчике). Запускаем опять игру, в меню ввода кода нажимаем клавишу, вводя символ предполагаемого кода, и программа стопорится на листинге:
листинг 6.
022E:9A57 FF06A4B1 inc word [B1A4] 022E:9A5B FEC3 inc bl 022E:9A5D 80FB04 cmp bl,04 022E:9A60 7303 jnc 00009A65 ($+3) 022E:9A62 E9B800 jmp 00009B1D ($+b8) DEBUG: Memory breakpoint: 0C11: B16C — 5B → 38
Инкрементируется какая-то переменная, затем происходит инкремент регистра и проверка, видимо, на превышение 0004, это логично связуется с необходимостью ввода 4 символов. По результатам проверки происходит переход либо на ret, либо на некую дополнительную последовательность команд. В том числе такую:
Переменная 0С11: B1B5 хранит введенный только что мною код »4789» в виде числа. Затем следует интересный участок:
листинг 7.
022E:9AAC 33D2 xor dx, dx 022E:9AAE 8BC2 mov ax, dx 022E:9AB0 E8A6F8 call 00009359 ($-75a) 022E:9AB3 3906B5B1 cmp [B1B5], ax 022E:9AB7 7420 je 00009AD9 ($+20) 022E:9AB9 42 inc dx 022E:9ABA 83FA14 cmp dx,0014 022E:9ABD 76EF jbe 00009AAE ($-11) Обнуляются dx и ax; вызывается подфункция, после чего недавно очищенный регистр ах сравнивается с введенным кодом (в численном виде). Если они идентичны, то происходит переход куда-то вдаль, иначе dx увеличивается на единицу, и, если не превышает 14, то операции повторяются.Логично предположить, что происходит перебор четырнадцати правильных паролей и сравнение с введенным (первые 9 уровней для сложности Beginner, остальные — для Expert, хотя эксклюзивных уровней для этой сложности только 2: Замок и Минотавр, чем отличаются другие уровни, не знаю. к примеру, по номеру 10 грузится Ледяной). Все факты подтверждают, что мы на правильном пути.
Собственно… все. Дальше идти некуда; достаточно изменить условный переход на безусловный. В принципе, было бы красиво изменить также количество вводимых символов, так как если оставить это без изменения, то придется вводить номер седьмого уровня как 0007 — ведь все же удобней будет вводить его как 07. Для этого изменим листинг 6 на следующий:
листинг 6_1.
022E:9A57 FF06A4B1 inc word [B1A4] 022E:9A5B FEC3 inc bl 022E:9A5D 80FB02 cmp bl,02 022E:9A60 7303 jnc 00009A65 ($+3) 022E:9A62 E9B800 jmp 00009B1D ($+b8) Искать, где и как рисуется строка из четырех черточек, чтобы заменить их на две, мне было уже лень. При том, что для отрисовки восклицательного знака из ресурса шрифтов Змиро использовал символ <, а для запятой — >, поиск «того, не знаю чего» мог оказаться весьма трудоемким. Потратив еще некоторое время на пошаговое исполнение в отладчике, можно было локализовать это место, но я не настолько перфекционист.Выводы. Что сделано Новый исполняемый файл игры prel.exe. Не забудьте проверить на вирусы. ;)Упразднена необходимость собирать пароли по уровням. Сделан возможным выбор уровня из главного меню посредством ввода его порядкового номера из двух разрядов вместо пароля из четырех. За ненадобностью убраны из маски вводимых символов буквы латинского алфавита (A-F), оставлены возможными только цифры; добавлена проверка диапазона вводимых номеров уровней (01–14). В самом деле, зачем игрокам по ошибке или от излишнего любопытства наблюдать вылет в «Insert disk with levelFF.sqz» вместо загрузки уровня. Пригласительная надпись в режиме ввода пароля с «ENTER CODE» изменена на «SELECT LVL». В самом начале из игры вырезано интро Hybrids. Изменено первое интро разработчика («Yeaaa… My game is still working in…»), — дополнено информацией о Hybrids и этой модификации. Изменены строки названий сложностей.Картинки кликабельны, особенно последняя.