Один раз - не Alkatraz
Территория острова Алькатрас использовалась как защитный форт, позже как военная тюрьма, а затем как сверхзащищённая тюрьма для особо опасных преступников и тех, кто совершал попытки побега из предыдущих мест заключения.
В настоящее время тюрьма расформирована, остров превращён в музей, куда ходит паром из Сан-Франциско от пирса номер 33.
Теперь иногда в голливудских фильмах появляется эта тюрьма, и, скорее всего, является древностью. Для нас, спектрумистов, название Alkatraz ассоциируется с защитой от копирования. Чем защита характеризуется? Нестандартный формат сигнала (http://www.worldofspectrum.org/tapsamp.html) и зашифрованная процедура загрузки. Благодаря World of Spectrum, защита существует в .tzx-форматах, например в Fairlight II:
http://www.worldofspectrum.org/infoseekid.cgi?id=0001715
Увидеть, как работает процедура, легко – поставьте точку прерывания отладчика эмулятора по адресу $5D5D.
После загрузки отладчик остановится на указанном адресе на команде RET. Я выполняю один шаг и вижу интересную процедуру:
После процедуры $9EF4-$9f09 оказывается нечитаемый мусор из команд. Это и есть одна из защит, если посмотреть на процедуру, которая называется ксорка (термин от битовой операции XOR – исключающее ИЛИ), то становится ясно, что опкоды команд дешифруются, после выполнения «расксоривания» процедуры появится следующее:
Часть блока дешифрована, и следующий участок явно выполняет те же действия. И такая защита идёт дальше и дальше, понадобится терпение, чтобы расшифровать весь кодовый блок. Впоследствии появились утилиты, которые помогают расшифровать блоки: XOR'EM ALL (https://zxpress.ru/article.php?id=6320) или утилита, применяющая похожий способ защиты – называлась, если не ошибаюсь LooksLikeShit.
Теперь, в наше время, такие защиты тоже можно отнести к древности, как и тюрьму на острове. Но речь пойдёт не об истории, а о другом.
Я собирал коллекцию crackintro от релизов TR-DOS, и часто наталкивался на различные способы защиты, которые применялись командами Jurassic Park и Golden Disk Corp.
Что самое интересное – это массовые процедуры, которые используют нестандартные приёмы. Я невольно задумался: как это реализовать? Ведь процедуры расшифровки повторяются несколько раз? Ничего я не придумал, и задумал свой вариант.
Я набрал свой код дешифровки, повторяющий варианты (void.asm):
;compile with sjasmplus
device zxspectrum128
ORG #6000
begin
di
ld hl,next1+1
xor a
ld b,a
ld c,b
ld r,a
lp1:
call $52;+2
dec sp;+1
dec sp;+1
pop de;+1
ld a,r;+2 ED operation
xor(hl)
xor d
xor e
xor c
xor b
ld (hl),a
inc bc
; ld r,a
inc l ;+1
next1:
jr nz,lp1;+1
inc h;+1
jr nz,lp1;+1
;вторая ксорка
ld hl,next2+1;+1
lp2:
call $52;+2
dec sp;+1
dec sp;+1
pop de;+1
ex de,hl
or a
sbc hl,de;+2 ED operation
ex de,hl
ld a,r;+2 ED operation
xor(hl)
xor d
xor e
xor c
xor b
ld (hl),a
dec bc
inc l ;+1
next2:
jr nz,lp2;+1
inc h;+1
jr nz,lp2;+1
; код расшифрован, демонстрация этого – распаковка картинки на экран
ld hl,pica,de,$4000:call dzx7_standard
;завершение исполнения
jr $
;сжатая картинка и распаковщик zx7
pica:incbin "Faded TNT Megademo2.scr.zx7"
zx7:include "zx7.a80"
end
display /d,end-begin
display next1+1
savebin "xorka.code",begin,end-begin
Один из хитроумных способов шифрования: применяются различные ключи, в примере Fairlight II используются данные в регистрах D и E. Как они работают? Операция XOR с числом дважды изменит значение: первый раз на другое, второй раз – на начальное значение. Получается, что в Fairlight II кодовый блок был зашифрован один раз, теперь процедуры расшифровывают коды повторно. Чтобы обойти ручную расшифровку, введены переменные значения – регистр BC, который изменяется во время работы от нуля до неизвестного значения.
Заодно применен регистр Z80 R – регистр регенерации памяти, 8 бит. Увеличивается на 1 после каждой выборки команды, но инкремент затрагивает только младшие 7 бит, старший бит не меняется и может быть использован в программах.
Увеличение на 1 зависит от операции. Например, при выполнении команд с префиксами #CB, #ED, #DD, #FD регистр R увеличится на 2.
Зная, как работает регистр R, я могу применить задуманное. Но для начала не помешает расписать принцип работы.
Почему использован ld hl,next+1 ? Взгляните на мою уже зашифрованную процедуру:
Что-то здесь не так. Ясно, что мусор ниже первой ксорки уже зашифрован, но переход JR NZ,$5FA0 ведёт в никуда. Так вот, этот трюк сделан, чтобы запутать юного хакера: по адресу $6019 хранится условный переход JR NZ, NNNN, который представляет собой два байта – $20 – это код, и второй байт – это относительное смещение. После первого прохода процедура дешифрует второй байт и занесёт его по нужному адресу ($601A), после чего процедура будет работать дальше.
Второй нюанс:
call $52;+2
dec sp;+1
dec sp;+1
pop de;+1
Это тоже малопонятно. Реализуется так: по адресу ПЗУ $0052 находится RET, на стеке сохранится адрес возврата $600C, следующие три команды извлекают этот адрес, DE=$600C, так осуществлена привязка декодирующей процедуры, в некоторых случаях будет трудно обойти первый участок кода.
Теперь, после прохода отладчиком несколько раз, видна картина:
Условие завершения работы ксорки понятно: расшифровка будет выполняться, пока HL<>0. Здесь можно поставить точку прерывания после завершения цикла – на $601E, и не прогонять ксорку постоянно. Хотя есть способ обмануть хакера, о нём я пока умолчу.
Итак, void.asm уже скомпилирован, имеется кодовый блок xorka.code, теперь нужно фрагменты зашифровать, пока непонятно, как? Первая процедура зашифрует код до неузнаваемости, вторая – тоже.
Наверное, нужно выполнить те же действия, написав ксорки на другом языке. Я выбрал PureBasic, потому что он поддерживает нужные побитовые операции. Согласно задуманным процедурам пишется код (genxor.pb):
Global r.a ; в переменной r будут храниться значения регистра R
*m=AllocateMemory(65536); общая память для Спектрума
Procedure incr(dr); процедура увеличения регистра R на dr – бит 7 сохраняется неизменным
r=(r&128)|( (r+dr)&127)
EndProcedure
;чтение готового файла ксорок
ReadFile(0,"xorka.code")
adr=$6000
While Not Eof(0)
PokeA(*m+adr,ReadAsciiCharacter(0))
adr+1
Wend
CloseFile(0)
;первая процедура
r=0
hl.u=$601A
bc.u=0
lp1:
; в комментариях написаны команды и приращение для регистра R
; это поможет при написании шифровки.
; lp1:
; call $52;+2
incr(2)
; dec sp;+1
; dec sp;+1
; pop de;+1
incr(3)
de.u=$600C
; ld a,r;+2 ED operation
incr(2)
a.a=r
; XOr(hl);+1
incr(1)
a=a!PeekA(*m+hl)
d.a=de/256
e.a=de&255
c.a=bc&255
b.a=bc/256
; XOr d;+1
a=a!d
incr(1)
; XOr e;+1
a=a!e
incr(1)
; XOr c;+1
a=a!c
incr(1)
; XOr b;+1
a=a!b
incr(1)
; ld (hl),a;+1
PokeA(*m+hl,a)
; inc bc;+1
bc+1
incr(2);?
; ld r,a
;r=a
; inc l ;+1
; next1:
; jr nz,lp1;+1
l.a=hl&255
h.a=hl/256
l+1
hl=h*256+l
incr(2)
If l<>0
Goto lp1
EndIf
; inc h;+1
; jr nz,lp1;+1
h+1
hl=h*256+l
incr(2)
If h<>0
Goto lp1
EndIf
; второй проход
; ld hl,next1+1;+1
hl=$6037
incr(1)
lp2:
; call $52;+2
; dec sp;+1
; dec sp;+1
; pop de;+1
incr(5)
;-de=$6024
; ex de,hl
; Or a
; sbc hl,de #ED
; ex de,hl
incr(5)
de=$6024-hl
; ld a,r;+2 ED operation
incr(2)
a=r
; XOr(hl)
incr(1)
a=a!PeekA(*m+hl)
d.a=de/256
e.a=de&255
c.a=bc&255
b.a=bc/256
; XOr d
a=a!d
incr(1)
; XOr e
a=a!e
incr(1)
; XOr c
a=a!c
incr(1)
; XOr b
a=a!b
incr(1)
; ld (hl),a
PokeA(*m+hl,a)
; dec bc
bc-1
incr(2);?
; inc l ;+1
; next2:
; jr nz,lp2;+1
l.a=hl&255
h.a=hl/256
l+1
hl=h*256+l
incr(2)
If l<>0
Goto lp2
EndIf
; inc h;+1
; jr nz,lp1;+1
h+1
hl=h*256+l
incr(2)
If h<>0
Goto lp2
EndIf
Теперь зашифрованная процедура уже готова, добавлю код (check.asm):
device zxspectrum128
ORG #6000
begin
incbin "xorka2.code"
end
display /d,end-begin
savesna "!check.sna",begin
Да, защита от любопытных глаз работает медленно, но постепенно выполняет свою работу.
Я не предлагаю готовое решение, я предлагаю идею. Возможно, когда-то и пригодится, хотя во время эмуляторов со всеми функциями подобная защита станет неактуальной.
Или эмуляторы останутся в прошлом, вслед за островом и защитой для копирования кассет, и «Спектрум» заменит PC.
Удачи!
Примечание редакции: исходники и прочие упомянутые в статье файлы можно скачать здесь:
http://dgmag.in/N20/alcatraz/filez.zip
|