ГЛАВА б
~~ ИСПОЛЬЗОВАНИЕ СИСТЕМНЫХ ПРОЦЕДУР
В предыдущем разделе были представлены процедуры из ROM: SAVE-
BYTES и LOAD-BYTtS. Здесь рассказывается, как эти процедуры использу-
ются для защиты программ. Займемся блоками машинного кода, которые
запускаются удивительным образом.
Первым таким способом является прикрытие загрузчика блоком, кото-
рый им загружается. Такая защита, к примеру, применяется в игре «три не-
дели в парадиз-сити». Проследим способ чтения этой программы, так, чтобы
она не стартовала автоматически.
Начнем, как обычно, с BASIC'a. Оказывается, практически, единственно
важной инструкцией является RANDOMIZE USR. Лучше всего восстановить
эту процедуру начиная с адреса запуска RANDOMIZE USR (PEEK
23627+25б*РЁЁК 23628), т.е., в нашем случае, с адреса 24130. Подобные про-
цедуры используют известную нам процедуру 1366, считывающую блоки без
заголовка. Так и в этом случае, но перед ее вызовом с помощью команды
LOIR процедура загрузки переносит сама себя в конец памяти (под адрес
63116) и переходит туда по команде JP:
24130 DI; запрет прерываний
24131LD SP,0; LD SP. 65536
24134 LD HL,(23627); в HL адрес на 28
24137 LD DE, 28; больше чем значение
24140 ADD HL, DE; переменной VARS
24141 LD <fpE, 63116; в DE адрес а в
24144 LD ВС, 196; ВС длина блока
ти iPhi 16; продолжение выполнения
программы с другого
24152 LD IX, 16384 ; адреса
24156 LD DE, 6912 ;
Теперь считывается картинка, а затем главный блок данных
Теперь считывается картинка, а затем главный блок данных
63116 |
LD |
IX,16384; |
подготовка загрузки |
63120 |
LD |
DE.69I2; |
картинки на экран |
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; |
программа |
Но способ запуска считанной программы требует пояснений. Как et'
знаете, каждая инструкция CALL заносит в машинный стек адрес, с которого
начинает работать программа после выхода из подпрограммы. В этом за
грузчике после выполнения второй инструкции CALL 13о6 в стек заносите*
адрес команды, следующей за CALL, т.е. 63142 и процедура загрузки самым
обычным способом затирает сама себя. т.к. считывает байты с магнитофона в
ту область памяти, где она была размещена. Существенное значение имев'
способ запуска считываемой программы: процедура 1366 кончается, естест-
венно, ийструкцией RET, которая означает переход по адресу, записанном>
в стек, или, в нашем случае, по адресу 63142. В процессе считывания прог-
раммы процедура, которая находилась там, -была заменена считанной прог-
раммой, но микропроцессор этого не замечает - он возвращается по адресу
с которого выполнен CALL 1366, не обращая внимания, что там находится
уже совершенно другая программа. Это схематично представлено на рис.1:
С левой стороны расписано содержимое памяти до, а с правой - после
считывания программы. Команды, отмеченные звездочками (*), ложатся на
выполняемую программу.
Возникает вопрос, как распознать защиту данного типа и как её
ликвидировать. Начинаем, естественно, с BASIC'a, считываем загрузчик (в
Ассемблере) и определяем адреса окончаний считанных блоков (добавляя к
адресу начала регистр IX длина блока / регистр DE). Если какой-либо из бло-
ков накрывает процедуру загрузки, то это означает, что программа считыва-
ется и запускается именно этим способом.
Дальше все просто. Достаточно, опираясь на данные о блоках (адрес и
длину), написать коротенькую процедуру, загружающую интересующий нас
блок кода или подготовить соответствующий заголовок, затем с помощью
CLEAR ADR установить соответствующим образом машинный стек (чтобы
считываемая программа не уничтожила стек) и, наконец, считать программу.
После выполнения в ней необходимых изменений записываем её на ленту,
но таким же образом, каким был записан оригинал (длина должна совпадать
прежде всего). Если этот блок был без заголовка (а так и есть в нашем слу-
чае), то записываем его обычным SAVE "..."CODE ..., но опуская заголовок,
т.е. включаем магнитофон только в перерыве между заголовком и блоков ко-
да. Также можно пробовать запускать считанный блок переходом на требуе-
мый адрес командой RANDOMIZE USR .... но это не всегда может
получиться. В игре «три недели в парадиз-сити» этим адресом конечно будет
63142 и как Вы можете убедиться - этот метод срабатывает.
Другим интересным способом запуска блоков машинного кода является
считывание программы в область машинного стека. Этим способом можно
запускать блокм машинного кода, загружая их просто через LOAD "" CODE.
Этот метод схематично представлен на рис.2:
МАШИННЫЙ СТЕК
МАШИННЫЙ СТЕК
65536 |
|
|
65537 |
|
|
per.SP—. |
1343 |
|
65359 |
|
|
65360 |
2053 |
|
65361 |
|
|
65362 |
7030" |
65368 |
65363 |
|
|
65364 |
4867 |
? |
65365 |
|
|
65366 |
? |
? |
RAMTOP— |
62 |
62 |
65368 |
|
LD IX, |
65369 |
|
|
65370 |
|
LD D E |
Рис. 2.
Рис. 2.
Указатель стека (регистр SP) принимает показанное на рис.2 состояние
в процессе выполнения процедуры 1366 (вызванной из Basic а через LOAD""
CODE). Способ запуска программы суммарно весьма прост. Адрес считы-
вания блока рассчитан так, что блок считывается на машинный стек именно с
того места, в котором находится (записанный интерпретатором BASIC'a) ад-
рес возврата из инструкции LOAD'^CODE (он тогда равен значению систем-
ной переменной ERRSP-2) или прямо из процедуры LOAD-BYTES (равный
ERRSP-6). Тогда 2 первых байта программы обозначают адрес её запуска.
Этот способ очень похож на предыдущий, за исключением того, что там под-
менялась процедура загрузки, а здесь - адрес возврата из этой процедуры
или просто адрес возврата из инструкции LOAD. После считывания блока ко-
да микропроцессор считывает содержимое стека и переходит по прочитан-
ному адресу (который только что появился в памяти вместо считанного). По
этому адресу в программе находится начало процедуры загрузки её последу-
ющих блоков - это видно на рис.2.
Метод обхода этой защиты также весьма прост. Достаточно заменить
RAMTOP на соответствующее низкое значение, а затем прочитать блок кода,
который благодаря этому не запустится. Ситуация может осложниться, если
блок очень длинный (что случается редко, но встречается) - тогда мы должны
поступить с ним так же, как и каждым длинным блоком, но помнить с какого
адреса он запускается.
Займемся теперь расчленением блоков кода с длиной, превышающей
42К. Взлом блоков этого типа основан на разделении их на такие фрагменты,
чтобы в памяти ещё осталось место для MONS'a или другого реассемблера,
исправление этих фрагментов, а затем их «склеивание» в одно целое либо
написание новой процедуры загрузки. Обычно достаточно разделить
длинный блок на две части. Чтобы получить первую, используем процедуру
1366, но с другими параметрами (не с теми, которых требует разделенный на
части блок. Ранее из процедуры загрузки или, если такой нет, то из заголовка
этого блока получаем его длину и адрес загрузки). Просто задаем адрес, по
которому хотим разместить этот блок (выше RAMTOP), а также длину
примерно 16К (несмотря на то, что блок этот значительно длиннее). Считыва-
ем теперь этот блок через CALL 1366 или CALL 2050, но во втором случае со-
общение «ТАРЕ LOADING ERROR», которое появится, не даст нам никакой
информации о верности считывания - загружается часть блока и, следова-
тельно, без контрольного байта, который находится в конце. Считанную
таким образом первую часть блока записываем на ленту и сразу же приступа-
ем ко второй части. Её считывание труднее, но тоже возможно, несмотря на
ограничения по памяти. Достаточно использовать тот факт, что в SPECTRUM
существует 16К ROM, запись в которые просто невозможна. Например, вы-
зываем процедуру 1366 с адресом чтения равным 0 и начальные 16К считы-
ваемого блока будет потеряно, а в память RAM считаются только следующие
32К или меньше (в зависимости от длины блока). Считываемый блок займет в
памяти RAM адреса с 16384 и дальше, заходя на системные переменные и
оставляя без изменения только те байты, адреса которых больше длины бло-
ка. Поэтому необходимо позаботиться о том, чтобы машинный стек, а также
написанную нами процедуру загрузки разместить в конце памяти. Надо так-
же помнить о том, что система bASIC'a будет уничтожена, и записать сразу
считанный блок на ленту можно только процедурой, написанной на Ассемб-
лере. Кроме того, в промежутках времени между считыванием фрагмента
блока и его записью нельзя разблокировать прерывания, так как они изменя-
ют содержимое ячеек с адресами 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, |
Dt; |
а затем входим |
LD A J 5; |
в её середину : |
OUT (254),A; |
|
CALL 1378; |
|
LD A,0; |
черная рамка |
JR C,OK; |
будет означать |
LD A,7; |
верное считывание, |
OK OUT (254),A; |
белая - ошибочное |
CZEKAJLD A, 191; |
ожидаем нажатия |
IN A, (254); |
«ENTER» |
RRA; |
|
JR C.CZEKAJ; |
|
LD IX,0; |
запись считанного |
LD DE,DL-16384; |
блока кода на |
LD A,255; |
ленту, |
CALL 1218; |
а также |
LD НЦ64999; |
инициализация |
JP 4633; |
системы Basic'а |
Вместо того, чтобы считывать Ассемблер и вводить эту программу, мож-
но запустить программу на BASIC'e, представленную на листинге 1:
ЛИСТИНГ 1
10 CLEAR 64999
20 INPUTDLINA BLOKA ?";DL: RANDOMISE DL: LET
X-PEEK 23670: LET Y-PEEK 23671: LET S-0
30 FOR N«65000 TO 65053: READ A: LET S-S+A: POKE N,A:
NEXT N
40 IF S 5254 + 2 • <X+Y)-64 THEN
PRINT "OSHIBKA V DANNYH": STOP
50 PRINT "WSE DANNYE ОКи '"WKLUCHI MAGN1TOPHON"
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
Kate выглядит разделение блока? Устанавливаем ленту на блоке кода, ко-
торый желаем разделить. Если он имел заголовок, то его опускаем. Запуска-
ем процедуру и включаем магнитофон. Не пугайтесь, если в определенный
момент Вы увидите, что программа считывается на экран - так должно быть.
Просле загрузки блока цвет рамки сигнализирует правильность считывания:
если рамка черная, то все в порядке, если белая - была ошибка. Тогда
вложите в магнитофон другую кассету, включите запись и нажмите «ЕЫТЕк».
Программа запишется на ленту, а потом процедура вернется в БЕЙСИК,
инициализируя систему сообщением «(С) 1982...», но не очищая память
(уничтожается только область от начала экрана до примерно 24000 адреса),
теперь, подготавливая заголовок или записывая коротенькую процедурку,
можно прочесть полученный блок под любой адрес. Когда Вы найдете то, что
искали и захотите запустить измененную программу, то придется немного
помучиться и «склеить» разделенную программу или написать для нее новую
процедуру загрузки. Если программа заполняла полностью 48К памяти RAM,
возможен лишь второй метод.
Если надо соединить блоки - достаточно написать процедуру, похожую
на разделяющую, но такую, которая считывает первый блок под адрес 16384,
второй - сразу за ним, а затем запишет их вместе как один блок.
В этом разделе был описан способ запуска блоков машинного кода
путем считывания в область машинного стека. Чтобы такой блок запустился,
достаточно ввести инструкцию LOADMMCODE, которая загрузит его. Чтобы
убедиться в этом на практике, введите и запустите программу с листинга 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 S3437 THEN PRINT "OSHIBKA V STROKE DATA": STOP
40 SAVE "BEEP" CODE 65362,34
50 DATA 88,255,3,19,0,62,221,2,29,6,8,197,17,30,0,33,112,5,205
60 DATA 181.3,33,0,8,43,124,18,1,32,251,193,16,235,221,225,201
Она запишет на ленте короткий блок машинного кода, который будет за-
пускаться самостоятельно. После записи этого блока освободите память с
помощью RANDOMIZE USR 0 или RESET (по возможности переставьте
RAMTOP на нормальное значение с помощью CLEAR 65367) и прочтите его,
вводя LOAD" "CODE. Цель этого блока - проинформировать о том, что он за-
пустился: он это делает несколькими звуковыми сигналами. Вы услышите их
сразу после считывания программы, как только с рамки исчезнут гранатово-
желтые полосы.