6. ЗАЩИТА ЗАГРУЗЧИКОВ
Все игры имеют хорошо защищенную программу, написанную на БЕИСИКе, т.к. это важнейший (с
точки действенности защиты) элемент всей программы. Ведь с БЕИСИКа начинается считывание всей про-
граммы. Если бейсиковский загрузчик защищен слабо, то взлом всей программы значительно облегчен.
Примером этого являются загрузчики фирмы ULTIMATE, представленные в предыдущем разделе. Одним
из способов снятия защиты загрузчиком является считывание их с помощью программы "LOAD/MERGE'"
(смотри раздел 2). Однако иногда лучше поместить этот загрузчик не в память, предназначенную для БЕИ-
СИКа, а выше RAMTOR, чтобы можно было спокойно рассматривать его не опасаясь возможности случай-
ных изменений в нем.
Для этого имеется очень действенный метод - считывание программы на БЕИСИКе как блока ма-
шинного кода под удобный для нас адрес. Чтобы этого добиться нужно, знать длину программы, которую
мы хотим считывать (можешь использовать процедуру "CZYTACZ" из раздела 2), хотя можно обойтись и
без длины. Кроме того, требуется немного свободного места на магнитной ленте. Этот способ основывает-
ся на обмане инструкции LOAD путем подмены заголовков.
На свободной ленте записывается заголовок блока кода с помощью SAVE "В AS" CODE 30000,750.
если мы знаем, что длина программы составляет 750 байт. Если мы ее не знаем - подаем соответственно
большее значение даже порядка нескольких десятков килобайт, хотя программа может иметь всего лишь
100 байт длины. На ленте записываем только сам заголовок, прерывая запись, после этого нажимаем кла-
вишу "BREAK". Теперь устанавливаем ленту точно перед записанным заголовком, а ленту с программой -
сразу после заголовка программы, но перед требуемым блоком данных, вводим:
CLEAR 2 99 99: LOAD "" CODE
или
CLEAR 29999: LOAD "" CODE 30000
и считываем заголовок.
Сразу после его считывания мы нажимаем СТОП в магнитофоне, заменяем кассету и вновь нажи-
маем ПУСК (все это время компьютер ждал блок данных). Теперь считывается программа на БЕИСИКе, но
под адрес 30000 - выше RAMTOR. Если мы задали в заголовке завышенную длину программы, то считыва-
ние кончается сообщением "Таре loading error", но это не мешает - теперь уже любым способом можно
смотреть считанную программу на БЕИСИКе. Кроме этого метода существует и второй, но для того чтобы
им пользоваться, обязательно знание АССЕМБЛЕРа (а пользоваться стоит, т. к. он дает большие возмож-
ности в раскрытии программ, а знание ее позволяет расшифровывать работу загрузчика.
Очень часто (особенно в новейших программах) встречаются блоки программ, записанные и считы-
ваемые в память компьютера без заголовка. Это достаточно оригинальная мера защиты обычно отпугивает
начинающих, но раскрытие такой программы не является вовсе трудным. Вся тайна основана на хранящих-
ся в ROM процедур, используемых с помощью инструкций LOAD, SAVE, VERIFY, MERGE.
Под адресом #0556 (1366) находится процедура LOAD-BYTES, считывающая с магнитофона блок
данных, или пилота и следующую за ним информацию. При этом не важно, будет ли это заголовок, или
требуемый блок данных, которые следует поместить где-то в памяти.
Начнем же сначала. Каждая защищенная программа начинается с загрузчика написанного на БЕИ-
СИКе. Программа, применяющая загрузку без заголовков (с помощью процедуры 1366 или другой), долж-
на быть написана на машинном коде, как каждая процедура, обслуживающая магнитофон. Чаще всего это
программа помещается в одной из строк БЕИСИКа. Например, после инструкции REM, или в области пе-
ременных БЕИСИКа. После считывания, загрузчик на БЕИСИКе запускается и выполняет инструкцию
RANDOMIZE USR ..., инициируя тем самым работу машинной программы. Процедура LOAD-BYTES тре-
бует соответствующих входных параметров. Они передаются в соответствующих регистрах микропроцес-
сора. Так в регистре IX задаем адрес, под который хотим прочесть блок данных, а в паре DE - длину этого
блока. В буфер помещаем 0, если хотим считать заголовок и 255 если это блок данных. Кроме того, указа-
тель переноса устанавливаем (CARRY) в 1, т.к. иначе процедура 1366 вместо LOAD выполнила бы функ-
цию VERIFY. Ниже дан пример процедуры, загружающейся с ленты без заголовка:
LD IX,16384 ; Адрес считывания
LD DE,6912 ; Длина блока
LD А,255 ; Блок данных
SCF ; Установка CARRY
CALL 13 66 ; Вызов LOAD-BYTES
RET ; Выход из подпрограммы
Процедура 1366 в случае ошибки считывания не выводит сообщение "Type loading error". Но суще-
ствует еще одна процедура загрузки, которая это делает. Она находится под адресом 2050 и выглядит так:
2650 CALL 1366 ; Считывание блока данных
2053 RET С ; Возврат, если не было ошибки
2054 RST 8 ; иначе RST 8 с сообщением
2 055 DEFB 2 6 ; "TYPE LOADING ERROR"
После возврата из процедуры 1366 указатель переноса содержит информацию о правильности счи-
тывания блока. Если он удалён, то это означает, что наступила ошибка. Некоторые загрузчики используют
процедуру 2050, а не 1366.
Иногда загрузчики не пользуются ни той ни другой процедурами, а заменяют их собственными, но
они однако, обычно очень похожи на процедуру 1366 или даже являются ее переделкой, благодаря которой,
например, блоки данных загружаются в нижнюю часть памяти - с больших адресов к нижним или загрузка
идет с другой скоростью. Такую программу следуёт анализировать с помощью дизассемблера (например,
MONS), сравнивая некоторые её фрагменты с тем, что находится в ROM.
Сейчас мы объясним, как использовать процедуру из ROM для считывания БЕИСИКа по любому
адресу, а не в область, предназначенную для него: сначала с помощью "SZYTACZ" прочитаем заголовок
программы, которую мы хотим вскрыть, и запоминаем ее длину (т.е. длину всего блока - программу вместе
с переменными). Затем вводим соответствующую программу, которая прочтет БЕИСИК под адрес, кото-
рый мы установим (выше RAMTOR):
LD IX,АДРЕС
LD DE,ДЛИНА
LD A,255
SCF
JP 2050
Также, как и при подмене заголовка, если мы не знаем длину программы, то можем задать завы-
шенное значение, но тогда чтение завершится сообщёнием "Type loading error". Но считывание ассемблера
каждый раз, чтобы ввести программу, приведенную выше, можно вызвать раздражение. Следовательно,
лучше создавать эту программу с уровня БЕИСИКа с помощью POKE:
10 INPUT "АДРЕС ЧТЕНИЯ BASIC?";A
2 0 RANDOMIZE A: CLEAR A-1
30 LET A=PEEK 23670: LET В=РЕЕК 23671
4 0 LET ADR=2 5 6 *B+A
50 INPUT "ДЛИНА BASIC ?";C
60 RANDOMIZE С: LET CHPEEK 23670
70 LET D=PEEK 23671
80 FOR N-ADR TO ADR+11
90 READ X: POKE N,X
100 NEXT N
110 DATE 221, 33,A,B,17,C,D, 62, 255, 195, 2, 8
12 0 RANDOMIZE USR ADR
Устанавливаем ленту с обрабатываемой программой за ее заголовком. Затем запускаем программу,
приведенную выше, вводим данные и включаем магнитофон. Результат аналогичен тому, который получа-
ем при подмене заголовков, но первым же видимым достоинством этого способа является то, что мы не
создаем беспорядок на кассетах.
В завершение стоит вспомнить еще об одной процедуре, размещенной в ROM под адресом 1218.
Это процедура SAVE-BYTES обратная LOAD-BYTES, т. е. записывающая на ленту блок с заданными па-
раметрами: перед ее выполнением в регистре IX размещаем адрес, с которого начинается здесь запись, DE
содержит длину записываемого блока. В буфере помечаем должен ли это быть заголовок (0) или блок про-
граммы (255). Состояние указателя CARRY значения не имеет.
7. ИСПОЛЬЗОВАНИЕ СИСТЕМНЫХ ПРОЦЕДУР
В предыдущем разделе были представлены процедуры из ROM: SAVE-BYTES и LOAD-BYTES.
Здесь рассказывается, как использовать эти процедуры для защиты программ. Займемся блоками машинно-
го кода, которые запускаются удивительным образом. Первым таким способом является прикрытие загруз-
чика блоком, который им загружается. Такая защита, к примеру, применяется в игре "ТРИ НЕДЕЛИ В ПА-
РАДИС". Проследим способ чтения этой программы так, чтобы она не стартовала автоматически.
Начнем, как обычно, с БЕИСИКа. Оказывается, практически, единственно важной инструкцией яв-
ляется RANDOMIZE USR. Лучше всего восстанавливать процедуру, начиная с адреса запуска
RANDOMIZE USR (PEEK 23627+256*РЕЕК 23628). Т.е. в нашем случае с адреса 24130. Подобные проце-
дуры используют известную нам процедуру 1366, считывающую блоки без заголовка. Так и в этом случае,
но перед ее вызовом с помощью команды LDIR, процедура загрузки переносит сама себя в конец памяти
под адрес 63116 и переходит туда по команде JP:
24130 DI ; 3апрет прерывания
24131 LD SP,0 ; LD SP,65536
24134 LD HL, (23627) ; B HL адрес на 28
24137 LD DE,28 ; Больше чем значение
2 414 0 ADD HL,DE ; Переменной VARS
24141 LD DE,63116 ; В DE Адрес, а в
24144 LD BC,196 ; BC длина блока
24147 LDIR ;
24149 JP 63116 ; продолжение выполнения
------------------; программы с другого
24152 LD IX,16384 ; адреса
24156 LD DE,6912
Теперь считывается картинка, а затем главный блок данных:
63116 LD IX,16384 ; Подготовка загрузки
63120 LD DE,6912 ; картинки на экран
63123 LD A,255 ; с помощью процедуры
63125 SCF ; LOAD-BYTES ИЗ ROM
63126 CALL 1366 ;
63129 LD IX,26490 ; Параметры главного
63133 LD DE,38582 ; блока программы,
63136 LD A,255 ; который считываясь
63138 SCF ; затирает эту процедуру
63139 CALL 1366 ; считывания блока
63142 JP NZ,+79 ; После возврата из
63144 CP A ; процедуры 1366 здесь
63146 CALL 65191 ; же находится другая
63149 JP NC,-2 ; программа
Но способ запуска считанной программы требует пояснения. Как вы знаете, каждая инструкция
CALL заносит в машинный стек адрес, с которого начинает работать программа после выхода из подпро-
граммы, в этом загрузчике после выполнения второй инструкции CALL 1366 в стек заносится адрес коман-
ды, за CALL, т.е. 63142 и процедура загрузки самым обычным способом затирает сама себя, т.к. считывает
байты с магнитофона в ту область памяти, где она была размещена. Существенное значение имеет способ
запуска считанной программы: процедура 1366 кончается естественно инструкцией RET, которая означает
переход по адресу, записанному в стек или в нашем случае по адресу 63142. В процессе считывания про-
граммы процедура, которая находилась там, была замещена считанной программой, но микропроцессор
этого не замечает - он возвращается по адресу, с которого выполнил CALL 1366, не обращая внимания, что
там находится уже совершенно другая программа. Это схематично представлено на рис 1.
С левой стороны расписано содержимое памяти до, а с правой после считывания программы. Ко-
манды, отмеченные звездочками (*), ложатся на выполняемую программу.
Возникает вопрос, как распознать защиту такого типа и как ее ликвидировать. Начнем с БЕИСИКа,
считываем загрузчик (в ассемблере) и определяем адрес окончания считанных блоков (добавляя к адресу
начала регистр IX длина/регистр DE). Если какой-либо из блоков накрывает процедуру загрузки, то это оз-
начает, что программа считывается и загружается именно таким способом.
Дальше все просто. Достаточно, опираясь на данные о блоках (адрес и длину), написать коротень-
кую процедуру, загружающую интересующий нас блок кода или подготовить соответствующий заголовок,
затем с помощью CLEAR ADR установить соответствующим образом машинный стек (чтобы машинная
программа не уничтожила стек) и, наконец, считать программу. После выполнения в ней необходимых из-
менений записываем ее на ленту, но таким же образом, каким был записан оригинал (длина блока должна
совпадать прежде всего!). Если этот блок был без заголовка (а так и есть в нашем случае), то записываем
его обычным SAVE"." CODE ..., но опуская заголовок, т.е. включаем магнитофон только в перерыве ме-
жду заголовком и блоком кода. Также можно пробовать запускать считанный блок перехода на требуемый
адрес командой RANDOMIZE USR., но это не всегда может получиться. В игре ТРИ НЕДЕЛИ В ПАРА-
ДИЗ" этим адресом будет 63142, и как ты можешь убедиться - этот метод срабатывает.
Другим интересным способом запуска блоков машинного кода является считывание программы в
область машинного стека. Этим способом можно запускать блоки машинного кода, загружая их просто че-
рез LOAD "" CODE. Этот метод показан схематично на рисунке 2.
МАШИННЫЙ СТЕК
Указатель стека (регистр SP) принимает показанное на рис 2. состояние в процессе выполнения
процедуры 1366 (вызванное из БЕИСИКа через LOAD "" CODE). Способ запуска программы суммарно
весьма прост. Адрес считывания блока рассчитан так, что блок считывается на машинный стек именно с
того места, в котором находится (записанный интерпретатором БЕИСИКа) адрес возврата из инструкции
LOAD "" CODE (он тогда равен значению системной переменной ERRSP-2) или прямо из процедуры
LOAD-BYTES (равный ERRSP-6). Тогда два первых байта программы обозначают адрес ее запуска. Этот
способ очень похож на предыдущий, за исключением того, что там подменялась процедура загрузки, а
здесь - адрес возврата из этой процедуры или просто адрес возврата из инструкции LOAD. После считыва-
ния блока кода, микропроцессор считывает содержимое стека и переходит по прочитанному адресу (кото-
рый только что появился в памяти). По этому адресу в программе находится начало процедуры загрузки её
последующих блоков. Это видно на рис 2.
Метод обхода такой защиты также весьма прост, достаточно заменить RAMTOR на соответственно
низкое значение, а затем прочитать блок кода, который благодаря этому не запустится. Ситуация может ос-
ложниться, если блок очень длинный (что случается очень редко, но встречается) - тогда мы должны по-
ступить также, как и с каждым длинным блоком, но помнить с какого адреса он запускается.
Займемся теперь расчленением блоков с длиной превышающей 42К. Взлом блоков этого типа осно-
ван на разделении их на такие фрагменты, чтобы в памяти еще осталось место для MONSa другого дизас-
семблера, исправления этих фрагментов, а затем их склеивания в одно целое либо написание новой проце-
дуры загрузки. Обычно достаточно разделить длинный блок две части. Чтобы получить первую, использу-
ем процедуру 1366, но с другими параметрами (не с теми, которых требует разделенный на части блок).
Ранее из процедуры загрузки или если такой нет, то из заголовка этого блока получаем ее длину и адрес за-
грузки. Просто задаем адрес, по которому хотим разместить блок (выше RAMTOR), а также длину, при-
мерно 16К (несмотря на то, что блок этот значительно длиннее). Считываем теперь этот блок через CALL
1366 или CALL 2050, но во втором случае сообщение "Таре loading error", которое появится, не даст нам ни
какой информации о верности считывания - загружаем часть блока и, следовательно, без контрольного бай-
та, который находится в конце. Считанную таким образом первую часть блока записываем на ленту и сразу
же приступаем ко второй части. Ее считывание труднее, но тоже возможно, несмотря на ограничения по
памяти. Достаточно использовать тот факт, что SPECTRUM существуют 16K ROM, запись, в которую не-
возможна. Например, вызываем процедуру 1366 с адресом равным 0 и начальные 16K будут просто поте-
ряны, а в память RAM считается только следующие 32К или меньше (в зависимости от длины блока). Счи-
тываемый блок займет в памяти RAM адрес с 16384 и дальше, заходя на системные переменные и оставляя
без изменения лишь те байты, адреса которых больше длины блока. Поэтому необходимо позаботиться о
том, чтобы машинный стек, а также написанную нами процедуру загрузки разместить в конце памяти. Надо
также помнить о том, что система БЕИСИКа будет уничтожена, и записать сразу считанный блок на ленту
можно только процедурой, написанной на ассемблере. Кроме того, в промежутках времени между считы-
ванием фрагмента блока и его записью нельзя разблокировать прерывания, т.к. они изменяют содержимое
ячеек с адресами 23552-23560, а также 23672-23673, а там находится считанный блок. Чтобы выполнить это
последнее условие войдем в середину процедуры 1366, благодаря чему после считывания блока не будет
выполнена процедура 1343. Именно она еще и разблокирует прерывания.
С помощью CLEAR 64999 переносим машинный стек, а с адреса 65500 помещаем процедуру за-
грузки:
ORG 65000
LD IX,0 ; Адрес считывания
LD DE,DL ; Длина блока
LD A,255 ; Подготовка к считыванию
SCF ; блока
INC D ; Таким способом
EX AF,AF ; заменяем начало
DEC D ; процедуры 1366
DI ; а затем входим
LD А,15 ; ее середину:
OUT (254),А
CALL 1378
LD A,0 ; Черная рамка
JR C,OK ; будет означать
LD А,7 ; верное считывание
OK OUT (254),А ; белая - ошибочное
CZEKAJ LD А,191 ; Ожидаем нажатие
IN A, (254) ;"ENTER"
RRA ;
JR C,CZEKAJ
LD IX,0 ; 3апись считанного
LD DE,DL-16384 ; блока на
LD A,255 ; ленту
CALL 1218 ; а также
LD HL,64999 ; инициализация
JR 4 633 ; системы БЕЙСИКА
Вместо того, чтобы считывать ассемблер и вводить эту программу, можно запустить программу на
БЕИСИКе, представленную на листинге 1:
Листинг 1
10 CLEAR 64999
20 INPUT "DLINA BLOKA?"; DL.RANDOMIZE DL: LET X=PEEK 23670: LET Y=PEEK
23671: LET S=0
30 FOR N=65000 TO 65054: READ A: LET S=S+A: POKE N,A: NEXT N
40 IF S<>5254+2*(X+Y)-64 THEN PRINT "OSIBKA V DANNYH": STOP
50 PRINT "WSE DANNYE OK=> WKLUCHI MAGNITOFON"
60 RANDOMIZE USR 65550
70 DATA 221, 33, 0, 0, 17, X, Y, 62, 2, 55, 55, 20, 8, 21, 243, 62, 15,
211, 254
80 DATA 205, 98, 5, 62, 0, 56, 2, 62, 7, 211, 254, 62, 191, 219, 254,
31, 56
90 DATA 249, 221, 33, 0, 0, 17, X, Y-64, 62, 255, 205, 194, 4, 33, 231,
253, 202, 25, 18
Как выглядит разделение блока? Устанавливаем ленту на блоке кода, который желаем разделить.
Если он имел заголовок, то его опускаем. Запускаем процедуру и включаем магнитофон. Не пугайся, если
увидишь в определенный момент, что программа считывается на экран - так и должно быть. После загрузки
блока цвет рамки сигнализирует правильность считывания: если рамка черная, то все в порядке, если белая,
то была ошибка. Тогда вложи в магнитофон другую кассету, включи запись и нажми "ENTER". Программа
запишет на ленту, потом процедура вернется в БЕЙСИК, инициализируя систему, сообщением "© 1982.",
но не очищая память (уничтожается только область от начала экрана до, примерно, 24000 адреса). Теперь,
подготавливая заголовок или записывая коротенькую процедуру, можно прочесть полученный блок под
любой адрес.
Когда ты найдешь то, что искал и захочешь запустить измененную программу, то придется немного
помучиться и "склеить" раздёленную программу или написать для нее новую процедуру загрузки. Если
программа заполняла полностью 48К памяти RAM возможен лишь второй метод.
Если надо соединить блоки - достаточно нависать процедуру, похожую на разделяющую, но такую,
которая считывает первый блок под адрес 16384, второй - сразу за ним, а затем запишет их вместе как один
блок.
В этом разделе был описан способ запуска блоков машинного кода путем считывания в область
машинного стека. Чтобы такой блок запустить, достаточно ввести инструкцию LOAD "" CODE, которая за-
пустит его. Чтобы убедиться в этом на практике введи и запусти программу с листинга 2:
Листинг 2
10 CLEAR 65361: LET S=0
20 FOR N=65362 TO 65395: READ A: POKE N,A: LET S=S+A: NEXT N
30 IF S<>3437 THEN PRINT "OSIBKA W STROKE DATA": STOP
40 SAVE "BEEP" CODE 65362,34
50 DATA 88,2 55,3,19,0,6,221,2,29,6,8,197,17,30,0,33,0,8,43,124,18
60 DATA 181,3,33,0,8,43,124,18,1,32,251,193,16,235,221,225,201
Она запишет на ленте короткий блок машинного кода, который будет запускаться самостоятельно.
После записи этого блока освободи память с помощью RANDOMIZE USR 0 или RESET (по возможности
переставь RAMTOR на нормальное значение с помощью CLEAR 65367) и прочти его, вводя LOAD ""
CODE. Цель этого блока проинформировать о том, что он запустился - он делает это несколькими звуко-
выми сигналами. Ты услышишь их сразу после считывания программы, как только с рамки исчезнут грана-
тово-желтые полосы.