8. ИСПОЛЬЗОВАНИЕ СИСТЕМНЫХ ПРОЦЕДУР
Ранее были представлены процедуры из ПЗУ:
SAVE7BYTES и LOAD-BYTES. Рассмотрим как использовать эти
процедуры для защиты программ. Рассмотрим блоки машинного
кода, запускаемые нестандартно.Первый способ- это прикрытие загрузчика блоком, который им
загружается. Такая защита применена, к примеру, в игре
ПАРАДИЗ-СИТИ. Проследим способ чтения этой программы, так,
чтобы она не стартовала автоматически. Начнем с бейсика.
Практически наиболее важной инструкцией является RANDOMIZE
USR. Лучше всего восстановить эту прцедуру с адреса запуска
RANDOMIZE USR (PEEK 2 3 6 4 7+2 5 6 * РЕЕК 23 62 8) , т.е. в нашем
случае с адреса 24130. Подобные процедуры используют из-
вестную нам процедуру 1366, считывающую блоки без заголовка.Также и в этом случае, но перед ее вызовом с помощью коман-
ды LDIR процедура загрузки переносит сама себя в конец
памяти по адресу 63116 и переходит туда по команде JP:![]()
потом считывается картинка, а затем главный блок данных:
потом считывается картинка, а затем главный блок данных:
![]()
Способ запуска считанной программы требует пояснения.
Как известно, каждая инструкция CALL заносит в машинный
стек адрес, с которого начинает работать программа после
выхода из подпрограммы. В этом загрузчике после выполнения
второй инструкции CALL 1366 в стек заносится адрес команды
за CALL. т.е. 63142,и процедура загрузки затирает сама себя,
так как считывает байты с магнитофона в ту область памяти,
где она была размешена. Важное значение имеет способ запус-
ка считанной программы. Процедура 1366 заканчивается инст-
рукцией RET. означающей переход по адресу , записанному в
стек, или, в нашем случае, по адресу 63142. В процессе счи-
тывания программы процедура, которая находилась там, была
замешена считанной программой, но микропроцессор этого не
замечает и возвращается по адресу, с которого выполнена
CALL 1366, не обращая внимания на то, что там находится
уже другая программа. Схематично это показано на рисунке:![]()
Слева расписано содержимое памяти до, а справа- после
считывания программы. Команды,отмеченные " * " ложатся на
выполняемую программу.Как распознать защиту такого рода и как ее обойти?
Начнем с бейсика. Считываем загрузчик (на ассемблере) и
определяем адрес окончания считанных блоков (добавляя к
адресу начала - регистр IX длину блока -регистр DE ). Если
какой-либо из блоков накрывает процедуру загрузки, то это
и означает, что программа считывается и загружается именно
таким способом. Дальше достаточно, опираясь на данные об
адресе и длине блока, написать короткую процедуру,
загружающую нужный нам блок кода, или подготовить
соответствующий заголовок, а затем с помощью CLEAR ADR
установить соответствующим образом машинный стек (так,
чтобы машинная программа не уничтожила стек), и, после
этого, считать программу. После выполнения в ней
необходимых изменений, ее можно записать на ленту, но в том
же виде, в каком был записан оригинал (длина блока должна
совпадать прежде всего ! ) . Если этот блок был без заголовка
( как в нашем случае) , то записываем его обычным SAVE ". . . "
CODE ... , но опуская заголовок, т.е. магнитофон включаем
только в перерыве между заголовком и блоком кода. Можно
также попробовать запустить считанный блок переходом на
требуемый адрес командой RANDOMIZE USR ... , но это не
всегда получается. В игре "ТРИ НЕДЕЛИ В ПАРАДИЗ СИТИ" этим
адресом будет 63142, и, можно убедиться - этот метод не
срабатывает.Другим интересным способом запуска блоков машинного
кода является считывание программы в область машинного
стека. Таким способом можно запускать блоки машинного кода,
загружая их просто через LOAD " " CODE. Схематично этот
метод показан на рисунке:Указатель стека (регистр SP) принимает показанное на
рисунке состояние в процессе выполнения процедуры 1366
(вызванное из бейсика через LOAD""CODE ). Способ запуска
программы в целом прост. Адрес считывания блока рассчитан
так, что блок считывается на машинный стек именно с того
места, в котором находится адрес возврата из инструкции
LOAD""CODE ( он при этом равен значению системной перемен-
ной ERRSP-2) или прямо из процедуры LOAD-BYTES (равен
ERRSP-6 ). Тогда два первых байта программы содержат адрес
ее запуска. Этот способ очень похож на предыдущий,за исклю-
чением того, что там подменялась процедура загрузки, а
здесь - адрес возврата из этой процедуры, или просто адрес
возврата из инструкции LOAD. После считывания блока кода
микропроцессор считывает содержимое стека и переходит по
прочитанному адресу, который только что появился в памяти.
По этому адресу в программе находится начало процедуры
загрузки ее последующих блоков - что и показано на рисунке.Для того,чтобы обойти такую защиту, достаточно заменить
RAMTOR на соответственно низкое значение, затем прочитать
блок кода, который из-за произведенной замены не запустится.- Ситуация может осложниться, если блок окажется очень длин-
ным ( что случается очень редко,но случается ) . В этом слу-
чае мы должны поступить с ним так же, как и с любым другим
длинным блоком, но помнить, с какого адреса он запускался.Теперь рассмотрим вопрос разделения на части блоков
длиной более 42К. Вмешательство в блоки такого типа основа-
но на разделении их на фрагменты так. чтобы в памяти еще
оставалось место для MONS-A или другого дизассемблера,
исправления этих фрагментов, а затем сборки их в целый блок
либо написания новой процедуры загрузки. Обычно достаточно
разделить блок на две части. Вначале необходимо из процедуры
загрузки, или, если ее нет. то из заголовка этого блока![]()
получить длину блока и адрес его загрузки. Чтобы полу-
чить первую часть блока используем процедуру 13 6 6 но с
другими параметрами- не с теми, которых требовал разделенный
на части блок,просто задаем адрес, по которому хотим размес-
тить этот блок (выше RAMTOR), а также длину - примерно 16К,
несмотря на то, что блок этот значительно длиннее. Считаем
теперь этот блок через CАLL 1366 или CALL 2050, но во вто-
ром случае сообщение "ТАРЕ LOADING ERROR", которое появит-
ся, не даст нам никакой информации о верности считывания,
т. к . мы загружаем часть блока без контрольного байта,
находящегося в конце блока. Считанную таким образом первую
часть блока записываем на ленту и сразу же приступаем ко
второй части. Ее считывание сложнее ввиду ограничений по
памяти, но также во зможно. Вспомним, что в нашем компьютере
16К ПЗУ, запись в которые нево змо жна. Например, вызовем
процедуру 1366 с адресом чтения ра вным 0, и начальные 16К
будут просто потеряны, а в память ОЗУ считаются только
следующие 32К или меньше (т.е. оставшаяся свыше 16K часть
блока) . Считываемый блок разместится в ОЗУ с адреса 16384,
заходя на системные переменные длины блока и оставляя без
изменения лишь те байты, адреса которых больше длины блока.
Поэтому необходимо позаботиться о том, чтобы машинный стек,
а также написанную нами процедуру загрузки разместить в
конце памяти. Нужно также помнить о том, что система
бейсика будет уничтожена и записать сразу считанный блок на
ленту можно только процедурой, написанной на ассемблере.
Кроме того в промежутках времени между считыванием фрагмента
блока и его записью нельзя разблокировать прерывания, так
как они изменяют содержимое ячеек с адресами 23552-23560 ,
а также 23672-23673, а там находится считанн ый блок. Чтобы
выполнить это последнее условие, войдем в середину процедуры
1366, благодаря че му после считывания блока не будет
выполнена процедура 1343. Именно она еще и разблокирует
прерывания. С помощью CLEAR 64999 переносим машинный стек,
с адреса 65500 помещаем процедуру загрузки:ORG 6 5 0 0 0;
LD IX,0; адрес считывания
LD DE,DL; длина блока
LD A,255; подготовка к считыванию
SCF; блока
INC D; таким способом
EX AF, AF; заменяем начало
DEC D; процедуры 1366
DI ; а затем входим
LD A, 15; в ее середину:
OUT (254) , A;
CALL 1378;
LD A, 0; черная рамка
JP 0, OK; будет означать
LD A, 7; верное считывание
OK OUT (254),A; белая - ошибочное
CZEKAJ LD A, 191; ожидаем нажатия
IN A, (254) ; "ENTER"
RRA;JP 0, CZEKAJ;
LD IX,0; запись считанного
LD DE,DL-16 3 8 4; блока на
LD A,255; ленту
CALL 1218; а также
LD HL,64999; инициализация
JP 4 633; системы бейсика .
Вместо того, чтобы считывать ассемблер и вводить эту
программу, можно запустить на ейсике программу,
представленную ниже:10 CLEAR 64999
20 INPUT "DLINA BLOKA?"; DL:RANDOMIZE DL
:LET X=PEEK 23670: LET Y=PEEK 23071: LET S=0
30 FOR N=65000 TO 65054: READ A:LET S=S+A
: POKE N, A : NEXT N40 IF S <>5254+2*(X+Y)-64 THEN
PRINT "ОШИБКА В ДАННЫХ": STOP5 0 PRINT "ВСЕ ДАННЫЕ О KEY, ВКЛЮЧИ МАГНИТОФОН"
60 RANDOMIZE USR 65500
7 0 DATA 221, 33 , 0, 0, 17, X, Y, 62, 2, 55 , 55, 20 , 8, 21, 2 43 , 62,
15,211,254
8 0 DATA 2 05,98,5,62,0,56,2,62,7,211,254,62,191,219,
254, 31,56
9 0 DATA 249, 221,33,0,0, 17, X, Y-64, 62, 255,205, 194,4,33,
231, 253 , 202 , 25, 18
Рассмотрим, как разделить блок. Установим ленту на блоке
кода, который нужно разделить. Если он имел заголовок, то
его опускаем. Запускаем процедуру и включаем магнитофон, в
определенный момент времени увидим, что программа считывает-
ся на экран - так и должно быть. После загрузки блока цвет
рамки говорит о правильности считывания: черная рамка-
все в порядке, белая - была ошибка. Вставим в магнитофон
другую кассету, включаем запись и нажимаем "ENTER"
Программа запишется на ленту, потом процедура вернется в
бейсик, инициализируя систему сообщением "(0) 1982..."
но не очищая память (уничтожается только область от начала
экрана до, примерно, 2 4 000 адреса) . Теперь подготавливая
заголовок или записывая коротенькую процедуру. можно
прочесть полученный блок под любой адрес.Чтобы запустить измененную программу придется немного
потрудиться и собрать разделенную программу или написать
для нее новую процедуру загрузки. Если программа заполняла
полностью 48К памяти ОЗУ, возможен только второй метод.Если надо соединить блоки - достаточно написать проце-
дуру, похожую на разделяющую, но такую, которая считывает
первый блок под адрес 163 84, второй - сразу за ним, а
затем запишет их вместе как один блок.6 этом разделе был описан способ запуска блоков машин-
ного кода путем считывания а область машинного стека. Для
запуска такого блока достаточно ввести инструкцию
LOAD""CODE, которая и запустит его.Чтобы убедиться в этом
на практике, запустим программу, приведенную ниже:10 CLEAR 65361: LET S=0
20 FOR N=65362 ТО 65395: READ A
:РОКЕ N,A: LET S = S+A: NEXT N
З0 IF S<>3437 THEN PRINT "ОШИБКА В СТРОКЕ DATA"
: стоп4 0 SAVE "BEEP" CODE 6 53 62,3 4
50 DATA 88,255,3,19,0,62,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
Она запишет на ленте короткий блок машинного кода,
который будет запускаться самостоятельно. После записи
этого блока освободим память с помощью RANDOHIZE USR 0
или RESET (по возможности, нужно переставить RAMTOR в
нормальное значение с помощью CLEAR 65367 ) и прочтем
его, введя LOAD""CODE. Задача этого блока - проинформи-
ровать, что он запустился - что и делается несколькими
звуковыми сигналами, которые появятся сразу после
считывания программы, как только с рамки исчезнут
гранатово - желтые полосы.