 |
"Od środka"
|  |
Od środka
Autorzy: Tomasz Surmacz, Robert Dudzik
Źródło: "Bajtek" 1-8/88
"Od środka" jest cyklem artykułów, które ukazały się w "Bajtku" w 1988r. Cykl ten dotyczy ściśle ZX Spectrum, jego systemu operacyjnego, a przede wszystkim zabezpieczeniom i łamniu zabezpieczeń programów pisanych w BASIC-u i kodzie maszynowym. Myślę, że jest to jeden z najciekawszych artykułów publikowanych w "Klanie Spectrum". Niestety nie udało mi się dotrzeć do autorów, więc zamieściłem cykl "Od środka" bez ich wiedzy i zgody (mam nadzieję, że nie mają mi tego za złe). Wszystkich, którzy pomogą mi skontaktować się z autorami proszę o pomoc.
Proszę też o wyrozumiałość i uwagi dot. błędów, gdyż teksty zostały przetworzone przez OCR, który ze swej natury nie jest doskonały.
Część I
Mało kto lubi programy. które przy pierwszym lepszym błędzie lub wciśnięciu klawisza BREAK czyszczą cała pamięć komputera. nie pozostawiając po sobie żadnego śladu. albo "zawieszają się", zmuszając do wciśnięcia RESET. Sytuacja przestaje być zabawna, gdy mamy jakiś dobry program użytkowy, który chcemy przystosować do nietypowego sprzętu (lub grę do rzadko spo-tykanego joysticka) lub gdy chcemy zmienić w programie wszystkie teksty angielskie na polskie, a program nie daje się zatrzymać.
Chcielibyśmy przedstawić wam kilkuodcinkowy cykl artykułów. Kolejno więc przedstawimy niezbędne przy takiej pracy informacje o twoim (lub może pożyczo-nym) komputerze, takie jak mapa pamięci, sposób za-pisu w pamięci poszczególnych linii BASIC-a, ważne zmienne systemowe, itp. Później zabierzemy się do wczytywania programów i bloków danych z taśmy w "bezpieczny" sposób, tzn. tak, by się nie uruchomiły i by można było obejrzeć ich zawartość. W końcu zaj-miemy się także unieszkodliwianiem zabezpieczeń pi-sanych w języku wewnętrznym. Postaramy się wszyst-ko to ilustrować konkretnymi przykładami, w oparciu o znane programy. Mamy nadzieję, że nasz wysiłek nie pójdzie na marne i ty także nauczysz się dostawać bez przeszkód do każdego programu.
Zacznijmy więc od podziału pamięci.
Ogólnie, pamięć podzielona jest na dwie główne części: ROM i RAM. ROM zajmuje adresy 0 - 16383, RAM natomiast adresy 16384 - 65535. Zawartością ROM-u nie będziemy się na razie zajmować, lecz za to przyjrzymy się dokładniej pamięci RAM. Jest ona po-dzielona na bloki spełniające różne funkcje w systemie BASIC-a (rys.1).
Pierwszym z nich jest obszar pamięci ekranu. Po-cząwszy od adresu 16384 znajduje się tzw. "display file"; czyli obszar, w którym przechowywane są infor-macje o tym, czy kolejne punkty ekranu są zapalone, czy zgaszone. Zajmuje to 6144 bajtów. Następne 768 bajtów (od adresu 22528) to komórki pamięci określa-jące kolory kolejnych pól ekranu (8 X 8 punktów). Ob-szar od adresu 23296 do 23551 to bufor drukarki. Jest on wykorzystywany tylko podczas współpracy kompu-tera z drukarką. Jeśli nie używasz instrukcji dotyczą-cych drukarki (takich jak LLIST, LPRINT, COPY), to jego zawartość nie ulega zmianie, możesz go więc wykorzystać do innych celów. Pamiętaj jednak, że użycie którejś z tych instrukcji, nawet bez podłączonej drukar-ki, wprowadza w tym obszarze zmiany.
Następnym fragmentem pamięci RAM są zmienne systemowe. Są to komórki pamięci wykorzystywane przez system do pamiętania niezbędnych do jego prawidłowego działania danych, takich jak np. adresy tzw. ruchomych bloków pamięci (o których zaraz powiemy), informacje o wykonywaniu programu w BASIC-u, tzn. która linia jest wykonywana, do której ma nastąpić skok, czy wystąpiły jakieś błędy, itp. W obszarze tym znajdują się także zmienne (tzn. komórki spełniające te funkcje), zawierające kod ostatnio wciśniętego klawi-sza, długość "beep" klawiatury i wiele jeszcze innych. Dokładniej zajmiemy się nimi później.
Bezpośrednio za zmiennymi systemowymi, które kończą się pod adresem 23733, zaczynają się tzw. ru-chome obszary pamięci. Oznacza to, że adresy ich po-czątków i końców (a także długość) mogą się zmieniać, w zależności od tego, czy są podłączone jakieś urzą-dzenia zewnętrzne, jak długi jest program w BASIC-u, ile tworzy zmiennych,. itp. Adresy ruchomych bloków RAM-u znajdują się w odpowiednich zmiennych systemowych.
Na rys. 1 i rys. 2 liczba pod strzałką oznacza adres początku wskazywanego bloku. Jeśli adres ten jest ru-chomy, to zamiast liczby zapisana jest nazwa zmiennej systemowej zawierającej ten adres, oraz w nawiasie -adres tej zmiennej. W nawiasie kwadratowym znajduje się wartość tej zmiennej ustalana zaraz po włączeniu komputera (lub wykonaniu RESET), ale bez podłączo-nych żadnych urządzeń zewnętrznych (czyli jeśli od-czytamy wartość zmiennej PROG, wykonując
PRINT PEEK 23635 + 256 * PEEK 23636, to otrzymamy wartość 23755).
Jeśli do twojego komputera podłączony jest "Interface 1 " lub interface innej szybkiej pamięci masowej, te od adresu 23734 do adresu o 1 mniejszego niż zawar-tość zmiennej CHANS, znajduje się "mapa microdri-ve'u" - obszar wykorzystywany jako bufor do tran-smisji danych, jako zbiór dodatkowych zmiennych sy-stemowych itp. Jeśli nie jest podłączone żadne z tych urządzeń, obszar ten po prostu nie istnieje - zmienna CHANS zawiera adres 23734. Określa ona początek bloku pamięci, w którym zawarte są informacje o istnie-jących kanałach. Są one konieczne do prawidłowego działania instrukcji PRINT, LIST, INPUT i podobnych. W ostatniej komórce tego obszaru znajduje się liczba 128 (heksadecymalnie 80), sygnalizująca koniec tego bloku (jest to tzw. znacznik końca). Następny obszar pamięci zawiera tekst wpisanego programu w BASIC-u. Adres jego początku pamiętany jest w zmiennej PROG. Bezpośrednio za tekstem programu (od adresu wskazywanego przez zmienną VARS), znajduje się obszar, w którym interpreter umieszcza zmienne tworzone przez program. Jest on zakończony znaczni-kiem końca. Następnie, począwszy od adresu zawartego w zmiennej E_LINE znajduje się obszar wykorzystywany podczas edycji linii BASIC-a oraz wpisywaniu komend z klawiatury (tzn. gdy na dole ekranu miga kursor i wpisujemy instrukcje w BASIC-u). Na końcu tego obszaru znajdują się dwa bajty, o zawartościach: 13 (ENTER - koniec linii) i 128 (koniec tego obszaru). Zaraz potem, od adresu wskazywanego przez zmienną systemową WORKSP, znajduje się podobny obszar, ale służący do wpisywania danych podczas wykonywa-nia przez interpreter instrukcji INPUT (zakończony znakiem ENTER).
Za buforem INPUT (który jest automatycznie kaso-wany po wykonaniu tej instrukcji) znajduje się "chwilo-wa przestrzeń pracy" - miejsce pamięci wykorzysty-wane do najrozmaitszych celów. Tam między innymi ładowane są nagłówki wczytywanych z taśmy progra-mów, tam jest wczytywany program umieszczany w pamięci przez MERGE"", zanim zostanie dołączony do już istniejącego programu. Obszar ten jest wykorzy-stywany wtedy, gdy na pewien czas potrzebujemy tro-chę wolnej pamięci, ale tylko do chwilowego wykorzy-stania - potem nie jest dla nas ważne co się z jej zawartością stanie.
Od adresu wskazywanego przez STKBOT, znajduje
się stos kalkulatora. Są tam odkładane liczby w trakcie wykonywania obliczeń przez interpreter BASIC-a. Stos ten rozrasta się w górę pamięci, tzn. w kierunku coraz wyższych adresów. Zmienna systemowa STKEND określa jego koniec. Za nim znajduje się obszar nie wykorzystywanej pamięci.
Do systemu BASIC-a należy jednak obszar aż do komórki pamięci wskazywanej przez zmienną syste-mową RAMTOP. Pod tym adresem znajduje się liczba 62 (3E hex), która oznacza koniec obszaru wykorzy-stywanego przez BASIC.
Idąc teraz w dół pamięci, trafiamy na jeden bajt nie wykorzystywany*. Zaraz za tym bajtem (idąc cały czas w dół pamięci), zaczyna się "stos GOSUB". Odkłada-ne są na nim numery linii programu, z których zostały wykonane instrukcje skoku do podprogramu, aby inter-preter wiedział dokąd ma "wrócić" instrukcja RETURN. Jeżeli interpreter nie znajduje się w żadnym podpro-gramie (wywołanym właśnie przez GOSUB), to stos ten po prostu nie istnieje - nie jest na nim zapisana żadna wartość. Niżej znajduje się stos maszynowy, wykorzystywany bezpośrednio przez mikroprocesor. Obydwa te stosy są odkładane w dół pamięci.
Specjalną rolę pełni zmienna systemowa ERRSP. Procedurą obsługująca błąd BASIC-a (wywoływana przez rozkaz mikroprocesora RST 8) umieszcza war-tość tej zmiennej w rejestrze SP, po czym wykonuje RET, odczytując w ten sposób ostatni zapisany na sto-sie adres (podczas wykonywania programu jest on rów-ny 4867). Pod tym adresem w ROM-ie znajduje się procedura drukująca komunikat o błędzie.
Powyżej komórki wskazywanej przez RAMTOP znaj-duje się 168 bajtów zarezerwowanych na definicje zna-ków UDG (można je zlikwidować np. przez CLEAR 65535). Adres ostatniej komórki pamięci (równy 65535; jeśli twój komputer jest całkowicie sprawny) jest pamiętany w zmiennej P_RAMT. Jeżeli część pa-mięci RAM jest uszkodzona, to zmienna ta zawiera ad-res ostatniej sprawnej komórki.
To by było wszystko, jeśli chodzi o podział pamięci Spectrum. Za miesiąc, zajmiemy się już włamywaniem do programów napisanych w BASIC-u oraz nagłówka-mi zbiorów zapisanych na taśmie.
* Bajt ten tworzy wraz z bajtem wskazywanym przez RAMTOP jakby jedną, dwubajtową liczbę (jest jej młodszym bajtem), konieczną do prawidłowego działania instrukcji RETURN. Gdy podczas jej wykonywania stos GOSUB-ów będzie jut pusty, to liczba ta spełni rolę jego przedłużenia. Ponieważ jednak jest ona większa nit i5872 (62~256), a linie BASIC-a nie posiadają tak wysokiej numeracji, więc zostanie to wykryte jako błąd i zasygnalizo-wane przez komunikat"RETURN without GOSUB".
Część II
Znacie już cały podział pamięci ZX Spectrum, mamy nadzieję, że zapamiętaliście coś z poprzedniego artykułu. A więc do dzieła. Dziś poznamy sposób zapisu programów na taśmie i zaczniemy się włamywać do programów napisanych w BASIC-u (pamiętaj, że praktycznie każdy program ma chociażby procedurę ładującą napisaną w BASIC-u, a jeśli jej nie posiada, to w zasadzie wcale nie jest zabezpieczony).
Na razie weźmy się jednak do spo-sobu zapisu programów na taśmie -przypomnij sobie co widać i słychać podczas wczytywania jakiegokolwiek programu.
Przez ok. 1 - 2 sekundy, na obrze-żu ekranu widoczne są szerokie, czerwono-niebieskie pasy oraz sły-chać ciągły dźwięk. Jest to tzw. pilot, który umożliwia komputerowi zsynch-ronizowanie się z sygnałem z taśmy, który ma odbierać. Potem przez krót-ką chwilę pojawiają się cienkie, miga-jące, żółto-granatowe paski, oznacza-jące, że komputer wczytuje do pamię-ci informacje. Jest ich 17 bajtów -tzw. nagłówek. Pojawia się napis "By-tes:" "Program:", "Character array:" lub "Number array:", a potem po chwili przerwy zaczyna się drugi pilot (nieco krótszy), a po nim wczytuje się właściwy program. Zajmiemy się teraz nagłówkami, gdyż zawarte są w nich najważniejsze informacje o wczytywa-nych programach.
Nagłówek - jak już wspomnieliśmy - składa się z 17 bajtów. Będziemy je numerować od 0 do 16 (patrz rys.1). Zerowy bajt oznacza typ bloku. Jest on równy 0 - jeśli jest to program w BASIC-u, 3 - jeśli blok kodu maszy-nowego (nagrany przez SAVE
..."CODE... albo SAVE "..."SCRE-EN$, które oznacza dokładnie to samo, co SAVE "..."CODE 16384,8912
Jeśli natomiast nagłó-wek ten poprzedza zbiór będący tabli-cą zmiennych BASIC-a (nagrany przez SAVE "..." DATA...), to jest on równy 1 - dla tablic liczbowych lub 2 - dla tablic znakowych.
Następne 10 bajtów, to nazwa wczytywanego bloku, czyli tekst poja-wiający się po załadowaniu nagłówka, za napisem "Program:", "Bytes:" czy innym.
Bajty 11. i 12. zawierają dwubajtową liczbę (pierwszy bajt mniej znaczący) oznaczającą długość bloku danych, którego ten nagłówek dotyczy.
W zależności od typu wczytywane-go bloku, bajty od trzynastego do sze-snastego są w różny sposób interpre-towane.
Zacznijmy od nagłówków progra-mów napisanych w BASIC-u. Bajty 13. i 14. oznaczają numer linii, od któ-rej ma się uruchomić program, jeśli był nagrany przez SAVE "..."LINE nr. Jeśli program był nagrany bez opcji LINE czyli po wczytaniu nie uruchomi się samoczynnie, to wartość tej liczby jest większa od 32767. Jednym ze sposobów unieszkodliwiania zabez-pieczeń samostartujących programów w BASIC-u jest zmiana tych dwóch bajtów w nagłówku na wartość więk-szą od 32767.
Bajty 15. i 16. zawierają liczbę oz-naczającą długość samego programu w BASIC-u (bo SAVE "" lub SAVE ""LINE nagrywają program wraz ze wszystkimi zmiennymi, tzn. zawartość pamięci od bajtu wskazywanego przez zmienną systemową PROG, do bajtu określonego zmienną E_LINE). Gdy od całkowitej długości bloku (baj-ty 11. i 12.) odejmiemy tę liczbę, do-wiemy się ile bajtów w tym bloku zaj-mują zmienne BASIC-a.
To wszystko, jeśli chodzi o nagłówki programów w BASIC-u.
W nagłówkach kodu maszynowego ("Bytes:") bajty 15. i 16. nie są wyko-rzystywane, natomiast 13. i 14. tworzą dwubajtową liczbę określającą pod jaki adres należy wczytać następujący po nagłówku blok.
W nagłówkach tablic wykorzystywa-ny jest spośród tych czterech, tylko bajt 14., który przedstawia nazwę wczytywanej tablicy. Jest on zapisany tak, jak nazwy wszystkich zmiennych BASIC-a (w obszarze od VARS do E_LINE), tzn. trzy najstarsze bity oz-naczają typ zmiennej (tutaj będzie to tablica liczbowa lub znakowa) a pięć mniej znaczących bitów - nazwę tej zmiennej*).
Na listingu 1. przedstawiony jest program tworzący procedurę umożli-wiającą odczytywanie nagłówków pro-gramów zapisanych na taśmie. Po jego dokładnym wpisaniu (uważaj na właściwą ilość spacji w liniach z instru-kcjami DATA!) uruchom go przez RUN i poczekaj chwilę. Na ekranie po-jawi się informacja o adresach począt-ku i końca procedury oraz o jej długo-ści, która powinna wynosić 284 bajty. Jeśli tak nie jest, sprawdź, czy na pe-wno wpisałeś wszystkie linie progra-mu. Jeśli wszystkie dane były popra-wne, to na twojej taśmie znajdzie się procedura "czytacz", dzięki której bę-dziesz mógł odczytać każdy nagłó-wek. Po nagraniu jej na taśmę możesz już skasować program z listingu 1.
Procedura "czytacz" jest relokowalna, tzn. będzie działała poprawnie niezależnie od tego, pod jakim adre-sem w pamięci się znajdzie. Możesz ją więc wczytać przez:
LOAD "czytacz" CODE adres a następnie uruchamiać przez RANDOMIZE USR adres
i to nawet wielokrotnie. Po uruchomie-niu, procedura wczytuje pod adres 23296 (do bufora drukarki) pierwszy napotkany nagłówek. Jeżeli ze wzglę-du na podłączone urządzenia zew-nętrzne nie odpowiada ci ten adres, to . możesz go zmienić zastępując w linii 200 programu z listingu 1. liczbę 005B heksadecymalnym adresem, pod któ-ry chciałbyś wczytywać nagłówki (dwie pierwsze cyfry są młodszym bajtem tego adresu). Aby po takiej zmianie uniknąć sprawdzania sumy kontrolnej w tej linii, na końcu tekstu
ujętego w cudzysłowy zamiast spacji i czterech cyfr sumy kontrolnej umieść literę "s" poprzedzoną czterema spacjami.
Po wczytaniu nagłówka procedura odczytuje informacje w nim zawarte i wraca do BASIC-a, lecz wczytanego nagłówka nie niszczy, więc jeśli chcesz, możesz go dodatkowo obej-rzeć wykorzystując funkcję PEEK.
Samo odczytywanie nagłówków to jednak trochę mało, by móc włamy-wać się do bloków zapisanych na taś-mie. Trzeba jeszcze wiedzieć, co z tymi blokami zrobić, aby umieścić je w -pamięci, nie pozwalając im się przy tym uruchomić.
W przypadku bloków typu "Bytes" wystarczy zwykle załadować je pod wymuszony adres, powyżej RAM-TOP-u (tzn. powyżej komórki pamięci wskazywanej przez zmienną syste-mową RAMTOP), np. przez:
CLEAR 29999: LOAD ""CODE 30000
Metoda ta skutkuje, jeśli tylko wczyty-wany blok nie jest zbyt długi (może mieć maksymalnie ok. 40K). Dłuższe bloki mogą się po prostu nie zmieścić w pamięci - konieczne jest wówczas podzielenie ich na kilka krótszych części. Niedługo dowiemy się jak to robić.
Podobnie, w przypadku tablic, zała-dowanie ich nie nastręcza raczej kło-potów - wystarczy użyć normalnej w takich wypadkach instrukcji LOAD "" DATA...
Gorzej natomiast wygląda wczyty-wanie programów w BASIC-u. Są one zwykle nagrywane przez SAVE "..."LINE, a na samym początku linii, od której mają się uruchomić umiesz-czone są instrukcje zabezpieczające je przed zatrzymaniem. Najprostszym rozwiązaniem jest ładowanie progra-mu nie przez LOAD "", lecz MERGE "", jednak metoda ta nie zawsze daje rezultaty. Z tej beznadziejnej wów-czas sytuacji istnieją dwa zasadnicze wyjścia: podmienić nagłówek progra-mu lub użyć przedstawionego niżej programu "load/merge".
Pierwsze z nich polega na zastąpie-niu nagranego na taśmie nagłówka przez taki sam, ale nie powodujący uruchomienia programu. Można w tym celu użyć programu "COPY COPY" - wczytać nagłówek progra-mu, wcisnąć BREAK, a następnie in-strukcją LET zmienić jego parametr START na liczbę większą od 32767 (tzn. wykonać np. LET 1="32768-jeśli przerabiany nagłówek był wczyta-ny jako pierwszy zbiór). Tak zmodyfi-kowany nagłówek nagrywamy gdzieś na taśmę. Usuwamy z pamięci pro-gram COPY COPY i wpisujemy LOAD " . Wczytujemy stworzony przed chwilą nagłówek, lecz zaraz po jego zakończeniu zatrzymujemy taśmę. Teraz do magnetofonu wkładamy ka-setę z programem, do którego się włamujemy, ale ustawioną zaraz za nagłówkiem tego programu - tak, aby wczytać tylko treść programu, bez oryginalnego nagłówka.
Druga metoda jest o wiele wygod-niejsza:
Wprowadzamy do pamięci (z kla-wiatury lub z taśmy) program "load/ merge" zamieszczony na listingu 2. Po uruchomieniu zaczyna on czekać na pierwszy program w BASIC-u, któ-ry znajduje się na taśmie. Wczytuje go zupełnie tak samo, jak instrukcja LOAD "", lecz po załadowaniu nie po-zwala na jego uruchomienie - druku-je jedynie komunikat "0 OK", z infor-macją od której linii wczytany program miał się uruchomić.
Na listingu 3. przedstawiona jest procedura, którą tworzy ten program poprzez POKE-i. Zasadniczą jej in-strukcją jest CALL 1821. Począwszy od adresu 1541, w ROM-ie znajduje się procedura interpretująca instrukcje SAVE, LOAD, VERIFY i MERGE. CALL 1821 wskakuje do środka tej procedury. Początkową jej część (od-czytującą parametry tych instrukcji) pomijamy, zastępując to w liniach 40 - 90 naszej procedury: Najpierw przez RST 48 rezerwujemy 34 bajty w obszarze WORKSP"), adres wolne-go miejsca przenosimy z rejestru DE do IX, w linii 80 zaznaczamy w zmien-nej systemowej TJ~DDR, że chodzi nam o LOAD, a nie np. VERIFY, a w pierwszym bajcie nagłówka do porów-nywania umieszczamy wartość 255, oznaczającą, że ma być wczytany pierwszy napotkany blok (tzn. pro-gram w BASIC-u). Reszty dokonuje procedura z ROM-u, działając identy-cznie jak LOAD "". Różnica w działa-niu LOAD a "load/merge" polega na tym, że po całkowitym wczytaniu pro-gramu, zamiast wrócić przez RET do interpretera BASIC-a, przepisujemy zawartość zmiennej NEWPCC ("nu-mer linii do której ma być skok") do PPC ("numer ostatnio wykonywanej linii") oraz nie dopuszczamy do uru-chomienia programu - wykonujemy RST 8 z komunikatem "0 OK". Dzięki temu przepisaniu, w wydrukowanym komunikacie znajdzie się informacja o " numerze linii, od której wczytany pro-gram miał się uruchomić.
Niecierpliwie czekając na kolejny odcinek z tego cyklu popróbuj odczy-tywać nagłówki bloków i wczytywać zabezpieczone programy w BASIC-u. Za miesiąc nimi właśnie dokładniej się zajmiemy.
* Sposób zapisu zmiennych BASIC-a w pamięci jest przedstawiony w dołączanym do Spectrum ang. Lub niemieckim podręczniku na stronach 122 -124, oraz w książce "Przewodnik po ZX Spectrum" na str. 71.
** Pierwsze 17 bajtów z tych 34 zostaje zapełnione przez procedurę ROM-u po odczytaniu parametrów wykonywanej instrukcji BASIC-a, drugie 17 to miejsce na wczytywany nagłówek. Obydwa te nagłówki są następnie porównywane, zanim zostanie wczytany blok danych (sprawdzana jest zgodność nazwy i typu bloku).
rys.1
Listing 1.
Listing 2.
Listing 3.
Część III
Po przeczytaniu ostatniego odcinka, wczytanie do pamięci programu tak, by się nie zdążył uruchomić, nie powinno być dla Ciebie żadnym problemem. choć wczytany program wcale nie musi wyglądać "normal-nie". ,
Na przykład w programie jest linia o numerze zero albo linie są uporządkowane z malejącymi nu-merami, nie można wykonać EDIT dla żadnej linii, widać podejrzaną instrukcję RANDOMIZE USR 0 lub po prostu nic nie widać, bo program nie daje się wylistować. Jeśli w programie, do którego się wła-mujesz, zauważyłeś coś dziwnego, to najlepiej obejrzyj go w trochę inny, niż normalny, sposób - -nie za pomocą LIST, lecz bezpośrednio - używa-jąc funkcji PEEK.
Najpierw jednak musimy się dowiedzieć, w jaki sposób jest umieszczany w pamięci tekst progra-mu w BASIC-u. Program składa się z kolejnych linii i tak też jest przechowywany w pamięci.
A oto, jak wygląda pojedyncza linia programu (rys. 1)
Zajmuje ona co najmniej 5 (a właściwie 6, bo tekst nie może być pusty) bajtów. Pierwsze dwa oznaczają jej numer, lecz uwaga! - odwrotnie niż wszystkie dwubajtowe liczby zapisane w pamięci - tutaj pierwszy bajt jest bardziej znaczący (MSB -Most Signifficant Byte), a drugi mniej znaczący (LSB - Less Signifficant Byte). Jeśli więc będą one przykładowo równe: pierwszy - 0, drugi -10, to nie będzie to oznaczało 2560 (0 + 10 * 256), lecz 10 (256 * 0 + 10).
Następne dwa bajty to długość linii, tzn. ile zna-ków zawiera tekst linii, wraz z kończącym go zna-kiem ENTER (heksadecymalnie OD). Za tymi baj-tami znajduje się już właściwy tekst linii, zakończo-ny przez ENTER. Jeśli wpiszemy np. taką linię:
10 REM BASIC
wyślemy ją wciskając klawisz ENTER, to w pamię-ci zostanie ona zapisana jako ciąg bajtów (rys. 2). Parametr "długość linii" dotyczy jedynie jej tek-stu, więc chociaż cała linia zajmuje w pamięci 11 bajtów, to parametr ten wskazuje tylko na 7 bajtów: 6 bajtów tekstu i siódmy - znak ENTER kończący linię.
Rozumiesz już chyba, na czym polega stosowa-ny często trick z linią o numerze zero. Wystarczy w dwa pierwsze bajty linii wpisać liczby 0 (za pomocą POKE-ów), by linia ta stała się linią zerową. Jeśli chcemy zmienić numer pierwszej linii w programie (a nie są podłączone interfejsy żadnej szybkiej pa-mięci masowej, bo wówczas zmienia się adres po-czątku BASIC-a), to wystarczy wpisać
POKE 23755,x: POKE 23756,y
a linia otrzyma numer 256 * x + y. Niezależnie je-dnak od tego, jaki on jest - pozostanie ona w pa-mięci tam, gdzie była. Jeśli więc wpiszemy np.
10 REM linia nr 10
20 REM linia nr 20
POKE 23755,0: POKE 23756,30
to pierwsza linia w programie otrzyma numer 30, pozostanie jednak w pamięci jako pierwsza, a na ekranie uzyskamy wydruk:
30 REM linia nr 10
20 REM linia nr 20
Aby więc zacząć odbezpieczać program, w któ-rym występują linie zerowe lub odwrotnie uporządkowane, należy poszukać adresów początków po-szczególnych linii i w ich polu "nr linii" umieszczać kolejno np. 10, 20, 30... W pamięci linie znajdują się jedna za drugą, więc z odnalezieniem ich po-czątków nie powinieneś mieć kłopotu. Jeżeli x wskazuje adres jakiejś linii programu, to adres na-stępnej jest równy: x + PEEK (x + 2) + 256 * PEEK (x +3) + 4 - do adresu linii dodajemy długość jej tekstu zwiększoną o 4 bajty, bo tyle zajmują parametry "nr linii" i "długość linii".
Taka metoda znajdowania początków linii nie skut-kuje niestety, gdy zastosowane jest drugie zabez-pieczenie - fałszywa długość linii. Polega ono na tym, że w polu "długość linii", zamiast prawdziwej wartości podana jest bardzo duża liczba - rzędu 43 - 65 tysięcy. Zabezpieczenie to jest bardzo często stosowane, gdyż zazwyczaj uniemożliwia wczytanie programu przez MERGE (czyli tak, by się nie uruchomił). Dzieje się tak dlatego, że MER-GE ładuje program z taśmy w obszar WORKSPA-CE, da następnie interpreter analizuje cały wczyta-ny program linia po linii: sprawdza kolejno numer każdej z nich, a następnie umieszcza ją w odpo-wiednim miejscu obszaru przeznaczonego na tekst programu w BASIC-u. Na linię tę musi przygoto-wać tam odpowiednią ilość wolnych bajtów, "roz-suwając" już istniejący tekst programu. Jeśli w polu "długość linii" podana będzie bardzo duża war-tość, to interpreter będzie usiłował zrobić właśnie tyle bajtów miejsca w obszarze tekstu programu w BASIC-u co skończy się komunikatem "Out of me-mory" lub po prostu zawieszeniem się systemu. Aby wczytać taki program nie powodując jego uru-chomienia, należy użyć odpowiedniego włamywa-cza, np. takiego, jak przedstawiony miesiąc temu program "load/merge".
Dodatkowym skutkiem podania fałszywej długo-ści, jest niemożność poprawiania takiej linii przez ściągnięcie jej do pola edytora klawiszem EDIT. Sytuacja przedstawia się podobnie: system opera-cyjny usiłuje zrobić miejsce na tę linię w obszarze edycji linii BASIC-a (od zmiennej E LINE do WORKSP patrz rys.2 w części I). Wymaga to jed-nak zbyt dużej ilości wolnej pamięci, więc kończy się to tylko ostrzegawczym dźwiękiem.
Jeżeli program zabezpieczony jest w ten spo-sób, trzeba adresów kolejnych linii szukać "ręcz-nie." lub domyślać się, gdzie one są, pamiętając o tym, że każda linia kończy się znakiem ENTER (ale nie każda liczba 13 oznacza ENTER.
Aby przeglądać program w BASIC-u, wpisz taką linię (listing C):
FOR n=23755 TO PEEK 23627+256*PEEK 23628 : PRINT n;" ";PEEK n,CHR$ PEEK n AND P EEK n>31: NEXT n
Wydrukuje ona kolejno: adres, zawartość bajtu o tym adresie oraz znak o tym kodzie, jeśli tylko nie jest to znak kontrolny (tzn. o kodzie 0-31)
Po zmianie numeracji linii i oszukiwaniu długości linii, następnym sposobem zabezpieczania progra-mów są znaki kontrolne, uniemożliwiające najczęś-ciej prawidłowe wylistowanie programu, choć nie tylko.
Wróćmy do pierwszego przykładu (linia "10 REM BASIC"). Tekst linii składał się z siedmiu znaków - słowa kluczowego REM" oraz pięciu liter i znaku ENTER. Tak dzieje się zawsze, jeśli w linii znajduje się instrukcja rct M - wszystkie znaki wpisane z klawia-tury, znajdujące się za tą instrukcją zostaną umie-szczone w tekście linii bez najmniejszych zmian.
-- Inaczej jednak przedstawia się sytuacja, gdy w linii znajdują się inne instrukcje, wymagające parame-trów liczbowych (a tak jest zazwyczaj). Wpiszmy np. linię:
10 PLOT 10,9
i zobaczmy w jaki sposób została zapisana w pa-mięci (najlepiej - wpisując podaną wyżej linię FOR n=23755 TO...). Wygląda ona w taki sposób, jak na rys. 3.
Jak widać tekst został zmodyfikowany - po ostatniej cyfrze każdej liczby występującej w tekście linii jako parametr, interpreter zrobił 6 bajtów miejsca i umieścił tam znak o kodzie 14 oraz pięć bajtów, w których zapisana jest wartość tej liczby, ale w spo-sób zrozumiały dla interpretera. Przyspiesza to w pewnym stopniu działanie programów w BASIC-u, ponieważ podczas działania programu interpreter nie musi za każdym razem przeliczać liczby z po-staci alfanumerycznej (tzn. ciągu cyfr) na pięciobaj-tową postać umożliwiającą wykorzystanie jej do obliczeń, lecz gotową wartość pobiera z pamięci, zza znaku kontrolnego CHR$ 14. Ten podwójny za-pis daje także duże możliwości utrudniania dostępów do programów. W wielu programach ładujących (tzw. ładowaczach lub loaderach) występuje taka linia:
0 RANDOMIZE USR 0: REM ...
Na pierwszy rzut oka - po uruchomieniu się, pro-gram ten powinien wykasować całą pamięć, tak się jednak nie dzieje. Po dokładniejszym obejrzeniu (przez PEEK - linią FOR n= 23755 ...) okazuje się, że po USR 0 i znaku CHR$ 14 wcale nie ma pięciu zer (bo tak w pięciobajtowym zapisie wyglą-da liczba zero)"", lecz np. 0, 0, 218, 92, 0, co jest równoznaczne liczbie 23770. Funkcja USR nie ska-cze więc pod adres 0, lecz właśnie 23770, a jest to adres bajtu znajdującego się zaraz za instrukcją REM w naszym przykładzie. Tam zwykle znajduje się program ładujący napisany w języku maszyno-wym.
Następnym znakiem kontrolnym, często stoso-wanym w różnych zabezpieczeniach, jest CHR$ 8 - "backspace", czyli spacja do tyłu. Wydrukowa-nie tego znaku powoduje cofnięcie pozycji wydruku o jeden znak w lewo. Można więc za jego pomocą zakrywać niektóre instrukcje na listingu, drukując w ich miejscu inny tekst. Jeśli np. w pamięci znajdują się kolejno znaki:
LET a=USR 0: LOAD "": ...
( {- oznacza CHR$ 8), to instrukcja LOAD "" i dalszy tekst zakryją wcześniejszą instrukcję LET a=USR 0. Chociaż na listingu widoczna jest tylko instrukcja LOAD "" , to dalsza część programu nie jest ładowana przez nią, lecz przez program maszynowy uruchamiany funkcją URS 0 (co nie musi oczywiście oznaczać skoku pod adres 0). Takie za-bezpieczenie jest np. stosowane w loaderze programu Beta Basic 1.0.
To by było wszystko na dzisiaj, choć znaków kontrolnych jest oczywiście więcej. Ich opis dokoń-czymy w następnym odcinku.
Tomasz Surmacz Robert Dudzik
Część IV
Ostatnio poznaliśmy sposób. w jaki przechowywana jest w pamięci każda linia BASIC-a oraz co oznaczają takie znaki kontrolne, jak CHR$ 14 i CHR$ 8. W tym odcinku poznamy po-zostałe znaki kontrolne. Trzy z nich dotyczą zmiany miejsca wydruku. sześć - zmiany atrybutów.
Pierwszym z nich jest CHR$ 6 - "COMMA CONTROL", czyli przecinek kontrolny. Ma on takie samo działanie, jak przecinek separujący teksty w instrukcji PRINT, tzn. drukuje tyle spacji (ale za-wsze co najmniej jedną), by znaleźć się w kolumnie O lub 16:
PRINT "1 ", "2"
oraz PRINT "1 "+CHR$ 6+"2" mają identyczne znaczenie.
Podobnie, znak CHR$ 22 - "AT CTRL", czyli AT kontrolne, pozwala przenosić pozycję wydruku w dowolne miejsce ekranu tak, jak AT w instrukcji PRINT. Po tym znaku muszą wystąpić dwa bajty, określające numer linii i numer kolumny, w której ma zostać umieszczony następny znak:
PRINT AT 10,7;"!" jest równoznaczne z
PRINT CHR$ 22; CHR$ 10; CHR$ 7; "!"
PRINT CHR$ 22;CHR$ 10;CHR$ 7;"!".
Aby się przekonać, jak za pomocą tego znaku uniemożliwić prawidłowy listing programu, wpisz np.:
10 RANDOMIZE USR 30000: REM __-Nic nie widać!,,
- po instrukcji REM wpisz trzy spacje, a po wy-krzykniku - dwa przecinki kontrolne. Można je uzyskać bezpośrednio z klawiatury, wciskając ko-lejno klawisze: EXTEND (lub dwa SHIFT-y na raz) by uzyskać kursor "E", a następnie klawisz "6" (kursor zmieni kolor na żółty) i DELETE - kursor przeskoczy do najbliższej połówki ekranu.
Po wpisaniu tej linii, wymieniamy te trzy spacje na znak AT 0,0 przez:
POKE 23774,22;POKE 23775,0: POKE 23776,0.
Spróbujmy teraz wylistować program. Na ekranie nie pojawia się tekst całej linii - początkowa jej część jest zakrywana przez napis znajdujący się za instrukcją REM i znakiem AT CTRL. Podobne kło-poty są gdy ściągniemy tę linię do edycji (klawisz EDIT).
Podane współrzędne w znaku AT CTRL powinny mieścić się na ekranie, tzn. numer linii nie może być większy niż 21, a numer kolumny - niż 31. Podanie większych wartości w przypadku PRINT lub LIST powoduje komunikat "Out of screen" i za-przestanie dalszego drukowania. Jeśli zaś listing uzyskano przez wciśnięcie ENTER (automatyczny listing) - także nastąpi zakończenie listowania programu, a ponadto na dole ekranu znajdzie się migający znak zapytania - sygnał błędu. Jest to więc praktyczny sposób na uniemożliwienie wyli-stowania każdego programu.
Kolejnym znakiem kontrolnym jest CHR$ 23 -"TAB CTRL" - znak tabulacji poziomej. Po nim następują dwa bajty określające numer kolumny, do której ma zostać przeniesiona pozycja wydruku. Są one traktowane jako jedna, dwubajtowa liczba (pierwszy bajt mniej znaczący). Ponieważ są jed-nak tylko 32 kolumny, to jest ona brana modulo 32, czyli jej starszy bajt i trzy najstarsze bity młodszego bajtu są ignorowane. Drugą istotną sprawą jest to, że TAB przenosi pozycję wydruku przez drukowa-nie spacji - podobnie jak przecinek kontrolny, może być więc użyty do zakrywania znajdujących się już na ekranie tekstów.
Następną grupę znaków kontrolnych stanowią znaki zmieniające atrybuty. Są to:
CHR$ 16-INK CTRL CHR$ 17 - PAPER CTRL CHR$ 18 - FLASH CTRL CHR$ 19 - BRIGHT CTRL CHR$ 20 - INVERSE CTRL i CHR$ 21 - OVER CTRL
Po każdym z tych znaków konieczny jest jeden bajt, precyzujący o jaki atrybut chodzi. Po znakach INK i PAPER mogą to być liczby 0 ... 9, po FLASH i BRIGHT: 0,1 i 8, po INVERSE i OVER : 0 i 1. Poda-nie innych wartości wywołuje komunikat "Invalid colour" i oczywiście przerwanie listowania progra-mu.
Odśmiecając program zabezpieczony znakami kolorów kontrolnych ustalamy sobie np., że jeśli przeglądając treść programu napotkamy kod znaku PAPER CTRL, to wpisujemy w jego drugim bajcie wartość 0, jeśli INK CTRL - wartość 7, a w pozo-stałe znaki kontrolne kolorów - wartość 0. Poza tym usuwamy wszystkie przeszkadzające znaki BACKSPACE (CHR$ 8) przez zastąpienie ich spacjami (CHR$ 32). Podobnie likwidujemy znaki AT CTRL-wymieniamy za pomocą POKE-ów trzy bajty znaku na spacje. Po takiej korekcie program daje się już wylistować bez żadnych niespodzia-nek.
Jeśli chcesz się włamać do programu ładujące-go, nie musisz, a w zasadzie nie powinieneś go od-śmiecać - ważne jest przecież to, aby dowiedzieć się co ten program robi, w jaki sposób ładuje do pa-mięci i uruchamia następne bloki, a nie aby robił to "ładnie" i był napisany czysto i przejrzyście. Jest to ważne tym bardziej, że zanim nie poznasz dokład-nie programu, lepiej nie rób w nim żadnych zmian - jedno zabezpieczenie może być sprawdzane przez inne. Dlatego najlepszym sposobem złama-nia programu jest analiza jego działania krok po kroku, odczytując kolejne bajty pamięci:
0: BORDER 0: PAER 0: INK 0: CLS :
PRINT #0,"LOADING";: FOR n=0 TO 20 STEP 4:
BEEP .2,n: NEXT n: LOAD "" CODE :
PRINT AT 19,0;:LOAD "" CODE : PRINT AT 19,0;:
LOAD "" CODE : PRINT AT 19,0;:
LOAD "" CODE : PRINT AT 19,0;:
LOAD "" CODE : PRINT AT 19,0;:
RANDOMIZE USR 24064
Pamiętaj o właściwej interpretacji kolejnych bajtów: najpierw dwa bajty numeru linii, potem dwa bajty oznaczające jej długość (które mogą być fałszywe), a następnie tekst: instrukcja BASIC-a, potem jej parametry, za każdą liczbą CHR$ 14 i pięć bajtów zawierających wartość tej liczby. Za parametrami -dwukropek i następna instrukcja lub ENTER i nowa linia programu.
To by było na tyle, jeśli chodzi o znaki kontrolne. Jest jednak jeszcze jedna rzecz, którą trzeba wyja-śnić, abyś nie miał kłopotów z odczytywaniem BA-SIC-a. Chodzi o instrukcję DEF FN. Wpisz, a nastę-pnie obejrzyj dokładnie taką linię:
10 DEF FN a(a,b$,c)=a+c
Wydaje się, że powinna ona zająć w pamięci 19 bajtów (numer linii, jej długość, ENTER oraz 14 wpisanych znaków), ale tak nie jest. Interpreter po każdym parametrze funkcji umieścił znak CHR$ 14 i zarezerwował na coś następne pięć bajtów. Na co? - zaraz się przekonasz. Wpisz:
PRINT FN a (1,"123",2
i ponownie dokładnie obejrzyj zawartość pamięci od adresu 23755. Po pierwszym parametrze w de-finicji funkcji dalej znajduje się CHR$ 14, ale po nim znalazły się kolejno: 0, 0, 1, 0, 0 co w pięciobajto-wym zapisie oznacza liczbę 1. Podobnie, po trze-cim parametrze funkcji znajduje się CHR$ 14 i bajty oznaczające liczbę 2. Po parametrze b$ także zna-lazła się wartość użytego parametru: CHR$ 14 i pięć bajtów, które kolejno oznaczają: pierwszy nie jest dla nas ważny, drugi i trzeci to adres, pod któ-rym znajdował się łańcuch "123" (wywołanie funk-cji nastąpiło w trybie bezpośrednim, więc adres ten dotyczy obszaru edycji linii BASIC-a), a bajty czwarty i piąty to długość łańcucha - w naszym przypadku wynosi ona 3 znaki.
Pamiętaj o tym odczytując BASIC przez PEEK a nie LIST. Zdarza się czasem, że w tych właśnie baj-tach zarezerwowanych na rzeczywiste wartości funkcji ukryte są zabezpieczenia warunkujące dzia-łanie programu lub nawet program maszynowy ła-dujący następne bloki (np. Beta, Basic 1.0).
Na zakończenie parę słów o programach ładują-cych - tzw. loaderach. Ich zadaniem jest wczyta-nie i uruchomienie wszystkich bloków składających się na program. Zazwyczaj robią to w sposób mak-symalnie utrudniający zrozumienie ich działania -tak, aby uruchomienie programu w inny sposób niż przez jego ładowacza (czyli w praktyce - włama-nie się do niego) było niemożliwe. Robią to w mniej lub bardziej wyrafinowany sposób. Spójrzmy na ła-dowacze stosowane w większości produktów firmy ULTIMATE (np. Atic Atac, Knightlore, Pentagram, Night Shade itp.). Wyglądają one mniej więcej tak: FOR n=23755 TO PEEK 23627+256*PEEK 23628 : PRINT n;" ";PEEK n,CHR$ PEEk: n AND P EEK n>31: NEXT n
Za takim ładowaczem znajduje się na taśmie pięć następnych bloków: ekran oraz zakodowany głów-ny blok programu, a za nimi trzy króciutkie bloki za-bezpieczające program: jednobajtowy (kod instruk-cji JP (HL) ), kilkunastobajtowy (jest to procedura odkodowująca cały program), oraz ostatni - dwu-bajtowy, ładowany pod adres 23672, czyli do zmiennej FRAMES. Zawartość tej zmiennej jest zwiększona o 1 co 1 /50 sekundy. W programie ma-szynowym uruchomionym przez RANDOMIZL USR 24064 wartość ta jest sprawdzana i jeśli je:;~ inna, niż być powinna (co oznacza, że w między-czasie, już po załadowaniu, program był' zatrzyma-ny na jakiś czas), następuje wyzerowanie kompute-ra. Włamanie się do tego typu programów jest pro-ste. Wystarczy załadować wszystkie bloki za wyjąt-kiem ostatniego, a po wylistowaniu programu, czy zrobieniu w nim odpowiednich zmian (np. wpisaniu POKE-ów unieśmiertelniających grę) wystarczy tyl-ko wpisać LOAD "" CODE: RANDOMIZE USR 24064 (ale koniecznie w jednej linii, rozdzielając in-strukcje dwukropkiem), by uruchomić grę.
Oddzielną sprawą jest odkodowanie programu, czyli uruchomienie procedurki odkodowującej w taki sposób, by po zakończeniu działania wróciła do BASIC-a.'Czytelnikom znającym asembler nie po-winno to sprawić kłopotu, jednak ze względu na powszechność stosowania tego typu zabezpie-czeń, szczególnie w programach użytkowych (np. Art Studio czy The Last Word), wrócimy jeszcze do tego tematu.
A za miesiąc zakończymy sprawę programów ła-dujących, tzn. ich BASIC-owej części i już na dobre
zajmiemy się assemblerem.
Tomasz Surmacz, Robert Dudzik
Część V
Cześć! W dzisiejszym odcin-ku powiemy sobie jeszcze tro-chę o BASIC-u, lecz zajmiemy się już także asemblerem, czyli tym, co dzieje się po wykona-niu w loaderze instrukcji RANDOMIZE USR...
Wszystkie gry mają bardzo dobrze zabezpieczo-ną swą część BASIC-ową, w końcu to najważniej-szy (pod względem skuteczności zabezpieczeń) element programu - od BASIC-a przecież zaczy-na się wczytywanie całego programu. Jeśli BASIC--owy loader jest słabo zabezpieczony, to włamanie się do całego programu jest znacznie ułatwione, na co przykładem są loadery firmy ULTIMATE, pre-zentowane miesiąc temu. Jedną z metod łamania zabezpieczeń loaderów jest wczytywanie ich za pomocą programu "load/merge" (patrz drugi odci-nek naszego cyklu). Jednak czasem lepiej jest umieścić ten loader nie w pamięci przeznaczonej na BASIC, lecz powyżej RAMTOP-u, by móc oglą-dać go bez żadnej obawy dokonania w nim przy-padkowych zmian.
Istnieje na to bardzo skuteczna metoda - wczy-tanie programu w BASIC-u, jako blok kodu maszy-nowego, pod wygodny dla nas adres. Aby tego do- konać, potrzebna jest znajomość długości progra-mu, który chcemy wczytać (możesz użyć procedu-ry "czytacz" z numeru 2/88 Bajtka), choć bez dłu-gości także można się obejść. Ponadto potrzebne jest trochę wolnego miejsca na taśmie magnetofo-nowej. Sposób ten polega na oszukaniu instrukcji LOAD przez podmienienie nagłówków.
Na wolnej taśmie nagrywamy nagłówek bloku kodu, przez SAVE "bas" CODE 30000,750, jeśli wiemy, że długość programu wynosi 750 bajtów. Jeśli nie znamy jej - podajemy odpowiednio więk-szą wartość - nawet rzędu kilkunastu lub kilku-dziesięciu kB, choć program może mieć raptem 100 bajtów. Na taśmę nagrywamy tylko sam nagłó-wek, przerywając później nagrywanie wciśnięciem BREAK. Teraz ustawiamy taśmę tuż przed nagra-nym właśnie nagłówkiem, a taśmę z programem -tuż za nagłówkiem programu, ale przed właściwym blokiem danych.
Wpisujemy:
CLEAR 29999: LOAD "" CODE
lub CLEAR 29999: LOAD "" CODE 30000
i wczytujemy nagłówek. Zaraz po jego wczytaniu wciskamy STOP w magnetofonie, wymieniamy ka-sety i ponownie wciskamy START (przez cały ten czas komputer czekał na blok danych). Teraz wczytuje się program BASIC-owy, ale pod adres 30000 - powyżej RAMTOP-u. Jeśli podaliśmy w nagłówku zawyżoną długość programu, to wczyty-wanie skończy się komunikatem "Tape loading er-ror", ale to nie szkodzi - nie oznacza to (najpraw-dopodobniej) błędu wczytania, lecz właśnie fakt zbyt małej ilości danych niż podana w nagłówku. Teraz już w dowolny sposób możesz oglądać wczytany program, nawet pisząc do tego celu włas-ny program w BASIC-u (nie musi to być jedna linia, do bezpośredniego wykonania, jak dotychczas):
Oprócz tej metody istnieje jeszcze druga, lecz by z niej korzystać, koniecznie trzeba znać asembler (a warto korzystać, bo daje ona większe możliwości łamania programów, a jej znajomość pozwala zwy-kle rozszyfrować, jak działa program ładujący).
Bardzo często (szczególnie w najnowszych pro-gramach) spotykane są bloki programów zapisane i wczytywane do pamięci komputera, bez nagłówka. Jest to dosyć oryginalne i efektowne zabezpiecze-nie, odstraszające zwykle początkujących, ale zła-manie takiego programu wcale nie jest trudne. Cała tajemnica polega na wykorzystaniu znajdujących się w ROM-ie Spectrum procedur, używanych przez instrukcje LOAD, SAVE, VERIFY i MERGE.
Pod adresem 1366 (hex 0556) znajduje się pro-cedura LOAD-BYTES wczytująca z magnetofonu blok danych, czyli pilota i następujące po nim infor-macje. Nie jest przy tym ważne, czy będzie to na-główek, czy też właściwy blok danych, które należy umieścić gdzieś w pamięci.
Zacznijmy jednak od początku. Każdy zabezpie-czony program rozpoczyna się od dłuższego lub krótszego loadera napisanego w BASIC-u. Pro-gram korzystający z ładowania bez nagłówków (przez procedurę 1366 lub inną) musi być napisany w kodzie maszynowym, jak każda procedura obsługująca magnetofon. Najczęściej program ten umieszczany jest w jednej z linii BASIC-a, np. po instru-kcji REM, lub w obszarze zmiennych BASIC-a. Po wczytaniu, BASIC-owy loader uruchamia się i wykonuje instrukcję RANDOMIZE USR ... inicjując tym samym działanie programu maszynowego.
Procedura LOAD-BYTES wymaga odpowiednich parametrów wejściowych. Przekazywane są one w odpowiednich rejestrach mikroprocesora, tak więc w rejestrze IX podajemy adres, pod który chcemy wczytać blok danych, a w parze DE, długość tego bloku. W akumulatorze umieszczamy 0 - jeśli chcemy wczytać nagłówek albo 255 - jeśli ma to być blok danych. Ponadto znacznik przeniesienia (CARRY) ustawiamy na 1, gdyż inaczej zamiast LOAD, procedura 1366 spełniałaby funkcję VERI-FY. Oto przykład procedury ładującej z taśmy obra-zek bez nagłówka:
LD IX,16384 ; adres wczytania
LD DE, 6912 ; długość bloku
LD A,255 ; blok danych
SCF ; ustaw CARRY, czyli LOAD
CALL 1366
RET ; powrót
Procedura 1366 w razie błędu wczytania, nie drukuje komunikatu "Tape loading error". Istnieje natomiast druga procedura ładująca, która to robi. Znajduje się ona pod adresem 2050, a wygląda tak:
2050 CALL 1366 ; wczytanie bloku danych
2053 RET C ; powrót, jeśli nie było
2054 RST 8 ; błędu, inaczej RST 8 z
2053 DEFB 26 ; komunikatem "Tape ..."
Po powrocie z procedury 1366, wskaźnik przenie-sienia zawiera informację o prawidłowości wczyta-nia bloku. Jeśli jest on skasowany, oznacza to, że wystąpił błąd. Niektóre loadery wykorzystują właś-nie procedurę 2050, a nie 1366.
Czasem procedury ładujące nie korzystają ani z jednej, ani z drugiej procedury, ale zastępują je własną, jest ona jednak zwykle bardzo podobna do procedury 1366 lub jest wręcz jej przeróbką, dzięki której np. bloki danych ładowane są w dół pamięci - od adresów wyższych do niższych, lub np. z inną prędkością. Taki program należy analizować za pomocą disassemblera (np. MONS-a) porównu-jąc niektóre jego fragmenty z tym, co znajduje się w ROM-ie (procedura 1366 była opisana szczegóło-wo w numerze 8/86 "Komputera").
A oto jak wykorzystać procedury z ROM-u do wczytania BASIC-a pod dowolny adres a nie w ob-szar dla niego przeznaczony: Najpierw za pomocą "czytacza" odczytujemy nagłówek programu, do którego chcemy się włamać i zapamiętujemy jego długość (tzn. długość całego bloku - programu wraz ze zmiennymi). Teraz wpisujemy odpowiedni program w asemblerze, który wczyta BASIC pod taki adres, jaki chcemy (powyżej RAMTOP-u):
LD IX,adres_wczytania
LD DE,długość bloku
LD A,255
SCF
JP 2050
Podobnie jak w sposobie z podmienianiem nagłów-ków, jeśli nie znamy długości programu - może-my podać wartość zawyżoną, lecz wówczas wczy-tanie zakończy się komunikatem "Tape loading er-ror". Każdorazowe wczytywanie asemblera po to, by wpisać powyższy program może być denerwu-jące, lepiej więc tworzyć go z poziomu BASIC-a poprzez POKE-i:
10 INPUT "Adres wczytania BASIC-a ? ";a
20 RANDOMIZE a: CLEAR a-1
30 LET a=PEEK 23670: LET b=PEEK 2 3671: LET adr=256*b+a
40 INPUT "długość BASIC-a ?";c
30 RANDOMIZE c: LET c=PEEK 23670: LET d=PEEK 23671
60 FOR n=adr TO adr+11
70 READ x: POKE n,x: NEXT n: DATA 221,33,a,b,17,c,d,62,233,195,2,8
80 RANDOMIZE USR adr
Ustawiamy taśmę z rozpracowywanym progra-mem za jego nagłówkiem. Teraz uruchamiamy po-wyższy program, podajemy dane i włączamy magnetofon. Skutek jest identyczny jak przy podmie-nianiu nagłówków, ale pierwszą widoczną zaletą tej metody jest to, że przy okazji nie robimy bałaganu na kasetach.
Na zakończenie wypada wspomnieć o jeszcze jednej procedurze, umieszczonej w ROM-ie pod adresem 1218. Jest to procedura SAVE-BYTES, odwrotna do LOAD-BYTES, tzn. nagrywająca na taśmę blok o podanych parametrach: przed jej wy-wołaniem w rejestrze IX umieszczamy adres od którego rozpocznie się nagrywanie, DE zawiera długość bloku do wysłania. W akumulatorze zazna-czamy czy ma to być nagłówek (0), czy blok pro-gramu (255). Stan wskaźnika CARRY nie jest waż-ny.
Za miesiąc pokażemy jak omawiane dziś procedury z ROM-u stosowane są w konkretnych pro-gramach.
Tomasz Surmacz, Robert Dudzik
Część VI
Miesiąc temu przedstawiiiś-my wam procedury z ROM-u -SAVE-BYTES i LOAD=BYTES. Dziś zobaczycie, jak procedury te wykorzystywane są w zabez-pieczeniach programów. Zaj-miemy się blokami kodu maszynowego, które uruchamiają się w dziwny sposób.
Pierwszym z tych sposobów jest przykrywanie pro-gramu ładującego blokiem, który jest przez nięgo łado-wany. Zabezpieczenie takie występuje np. w grze "Three Weeks in Paradise": Prześledźmy sposób wczytywania tego programu, tak, by się nie uruchomił.
Zaczynamy jak zwykle od BASIC-u i przystępujemy do jego oglądania. Okazuje się, że praktycznie jedyną ważną instrukcją jest RANDOMIZE USR w obszar zmiennych BASIC-a, czyli tam zńajduje się procedura ładująca, napisana w asemblerze. Musimy więc zająć się nią.
Najlepiej jest zdisasemblować tę procedurę począ-wszy od adresu, od którego jest uruchamiana instrukc-ją RANDOMIZE USR (PEEK 23627+256 PEEK 23628), tzn. w naszym przypadku, od adresu 24130. Procedury takie wykorzystują zwykle znaną nam pro-cedurę 1366, wczytującą bloki bez nagtówka. Tak jest i tu, ale przed jej wywołaniem., za pomocą rozkazu LDIR, procedura ładująca przenosi samą siebie na koniec pa-mięci (pod adres 63116) i skacze tam instrukcją JP:
24130 DI ;Zabronienie przerwań
24131 LD SP, O ; - Tzn. LD SP, 63336.
24134 LD HL,(23627);Do HL adres o 28
24137 LD DE,28 ;większy, niż wartośt
24140 ADD HL.,DE ;zmiennej syst. VARS.
24141 LD DE,63116 ;Do DE adres, a do HC
24144 LD BC,196 ;długość bloku, który
24147 LDIR ;zostaje skopiowany.
24149 JP 63116 ;Kontynuacja wykony
24152 LD IX,16384 ;wania programu pod
24156 LD DE,6912 ;innym adresem.
Teraz wczytuje obrazek, a następnie główny blok danych:
63116 LD IX,16384 ;Przygotowania do za-
63120 LD DE,6912 ;ładowania obrazka na
63123 LD A,255 ;ekran, poprzez pro-
63125 SCF ;cedurę LOAD-BYTES z
63126 CALL 1366 ;ROM-u.
63129 LD IX,26490 ;Parametry głównego
63133 LD DE,38582 ;bloku programu,który
63136 LD A,255 ;wczytujqc się,kasuje
63138 SCF ;tę procedurę.
63139 CALL 1366 ;wczytanie tego bloku
63142 JR NZ,+79 ;Po powrocie z proce-
63144 CP A ;dury 1366 znajduje
63146 CALL 65191 ;się tu już inny pro-
63149 JR NC,-2 ;gram.
Wyjaśnienia wymaga jednak sposób, w jaki uruchamia się wczytany program. Jak zapewne wiecie, każda in-strukcja CALL odkłada na stosie maszynowym adres, od którego ma działać program po powrocie z podpro-gramu. W tym loaderze, po wykonaniu drugiej instruk-cji CALL 1366, na stosie odkładany jest adres instrukcji następnej po CALL, tzn. 63142, a procedura ładująca najzwyczajniej w świecie kasuje samą siebie, bo bajty z magnetofonu wczytują się w ten sam obszar pamięci, gdzie była ona umieszczona - wczytywany program "przykrywa" procedurę ładującą. Najistotniejszą rzeczą jest sposób, w jaki wczytany program się uruchamia: Procedura 1366 kończy się oczywiście instrukcją RET, która oznacza skok pod adres zapisany na stosie, czyli w naszym przypadku, pod adres 63142. W trakcie wczytania programu procedura, która się tam znajdo-wała, została podmieniona przez wczytany właśnie program, ale mikroprocesor nie zauważa tego-wraca pod adres; z którego wykonano CALL 1366, nie zwra-cając uwagi na to, że znajduje się tam już zupełnie inny program. Schematycznie przedstawia to rys.1. Z lewej strony rozpisana jest zawartość pamięci, przed, a z pra-wej - po wczytaniu programu. Żółtym kolorem zazna-czone są instrukcje, składające się na wykonywany program.
A oto, jak rozpoznać tego typu zabezpieczenia, i jak: je zlikwidować. Zaczynamy oczywiście od BASIC-a, odczytujemy procedurę ładującą (w asemblerze) i; liczymy adresy końców wczytywanych bloków (dodając do adresu początku (rejestro~17C długość bloku rejestr DE))~-Jeśli którykolwiek z bloków zachodzi na proce-durę ładującą, oznacza to, że program wczytuje się i uruchamia właśnie w ten sposób.
Reszta jest już prosta. Wystarczy, w oparciu o dane bloków (adres, i długość), napisać króciutką procedurę ładującą interesujący nas blok kodu lub spreparować odpowiedni nagłówek, następnie przez CLEAR adr od-powiednio ustawić stos maszynowy (aby wczytany program nie zniszczył stosu) i wreszcie - wczytać program. Po wykonaniu w nim odpowiednich zmian, nagrywamy go na taśmę, ale tak samo, jak byt on zapi-sany oryginalnie (zgadzać się musi przede wszystkim długość). Jeśli blok ten był bez nagłówka (a tak jest w naszym przypadku), to nagrywamy go przez zwykłe SAVE "..."CODE..., ale pomijając nagłówek, tzn. uru-chamiamy magnetofon dopiero w przerwie między na-główkiem, a blokiem kodu. Można także próbować uru-chomić wczytany blok skacząc pod odpowiedni adres instrukcją RANDOMIZE USR..., lecz nie zawsze musi się to udać. W grze "Three Weeks in Paradise" adre-sem tym będzie oczywiście 63142 i jak możesz się przekonać - metoda ta skutkuje.
Innym ciekawym sposobem uruchamiania bloków kodu maszynowego jest wczytywanie programu w ob-szar stosu maszynowego. W ten sposób można uru-chamiać bloki kodu maszynowego, ładując je po prostu przez LOAD " "CODE! Metoda ta została schematycz-nie przedstawiona na rys.2. Wskaźnik stosu (rejestr SP) przyjmuje pokazaną na rysunku wartość w trakcie wykonywania procedury 1366 (wywołanej z BASIC-a przez LOAD ""CODE).
Sposób, w jaki program się uruchamia, jest w sumie bardzo prosty. Adres wczytywania bloku jest tak wyli-czony, że wczytuje się on na stos maszynowy, dokład-nie od tego miejsca, w którym znajduje się (zapisany przez interpreter BASIC-a) adres powrotu z instrukcji LOAD ""CODE (jest on wtedy równy wartości zmien-nej systemowej ERRSP - 2) lub wręcz z procedury LOAD-BYTES (równy ERRSP - 6). Wówczas dwa pierwsze bajty programu oznaczają adres jego urucho-mienia. Sposób ten jest bardzo podobny do poprzed-niego, tylko że tam podmieniana była procedura ładują-. ca, tutaj-adres powrotu z tej procedury lub po prostu adres powrotu z instrukcji LOAD. Po wczytaniu bloku kodu, mikrokomputer odczytuje zawartość stosu i ska-cze pod odczytany adres (który dopiero co znalazł się w pamięci wraz z wczytanym programem). Pod tym ad-resem znajduje się w programie początek procedury ładującej kolejne jego bloki - tak jak to widać na rys.2.
Metoda ominięcia tego zabezpieczenia także jest do-syć prosta. Wystarczy zmienić RAMTOP na odpowied-nio niską wartość, a następnie wczytać blok kodu, który dzięki temu nie uruchomi się. Sytuacja komplikuje się, jeśli blok ten jest bardzo długi (co zdarza się rzadko, ale jest możliwe) - wtedy musimy z nim postępować tak, jak z każdym zbyt długim blokiem, ale pamiętając, w jaki sposób się uruchamiał, (tzn. od jakiego adresu).
Zajmiemy się teraz właśnie cięciem bloków kodu o długości przekraczającej 42K. Włamywanie się do tego typu bloków polega na podzieleniu ich na takie frag-menty, by w pamięci pozostało jeszcze miejsce na MONS-a czy inny disasembler, poprawieniu tych frag-mentów, a następnie "sklejeniu" ich w jedną całość lub. napisaniu nowej procedury ładującej. Zazwyczaj wystarcza, jeśli długi blok podzielimy na dwie części. Aby otrzymać pierwszą z nich, wykorzystujemy proce-durę 1366 ale z innymi parametrami niż wymaga tego dzielony na części blok (wcześniej oczywiście z proce-dury ładującej, lub jeśli takiej nie ma - z nagłówka tego bloku, odczytujemy jego długość i adres wczyta-nia). Podajemy po prostu adres, pod jaki chcemy ten blok wczytać (powyżej RAMTOP-u!) oraz długość rów-ną około 16K (mimo tego, że blok ten jest znacznie dłuższy). Wczytujemy teraz ten blok przez CA'LL 1366 lub CALL 2050, lecz w drugim przypadku komunikat "Tape loading error", który się ponowi, nie da nam ża-dnej informacji o poprawności wczytania - ładujemy tylko część bloku, a więc bez bajtu kontrolnego, który umieszczony jest na końcu. Tak wczytaną pierwszą część bloku nagrywamy na taśmę a na razie zabieramy się do drugiej części, Jej wczytanie jest trudniejsze, ale też możliwe, mimo ograniczeń pojemności pamięci. Wystarczy wykorzystać fakt, że w Spectrum istnieje 16K ROM-u, a próba zapisu informacji do ROM-u po prostu nic nie daje. Jeśli więc np. wywołamy procedurę 1366 z adresem wczytania równym np. O, to początko-we 16K wczytywanego bloku zostanie stracone, a do pamięci RAM wczyta się TYLKO końcowe 32K lub mniej (zależnie od długości bloku). Nie wystarczy jed-nak napisanie tak prostej procedurki, jak poprzednio. Wczytywany blok zajmie w pamięci RAM adresy od 16384 i dalej, zachodząc na zmienne systemowe, pozostawiając bez zmian jedynie bajty o adresach więk-szych niż jego długość. Dlatego też musimy zadbać o to; by stos maszynowy oraz napisaną przez nas proce-durę ładującą umieścić na końcu pamięci. Musimy tak-że pamiętać o tym, że system BASIC-a zostaje znisz-czony i musimy wczytany blok od razu nagrać na taś-mę procedurę napisaną w asemblerze. Ponadto w cza-sie między wczytaniem fragmentu bloku a jego nagra-niem,. nie można do odblokowania przerwań, gdyż zmieniają one wartość komórek o adresach 23552 -23560, oraz 23672 - 23673, a tam znajduje się wczy-tany blok. Aby spełnić ten ostatni warunek, wskoczymy do środka procedury 1366, dzięki czemu po wczytaniu bloku nie zostanie wykonana procedura 1343, która właśnie m.in. odblokowuje przerwania. Przez CLEAR 64999 przenosimy stos maszynowy, a od adresu 65500 umieszczamy procedurę ładującą:
10 ORG 65000
20 LD IX,O ;Adres wczytania.
30 LD DE,dl ;Długość bloku.
40 LD A,255 ;Przygotowania do
50 SCF ;wczytania bloku.
60 INC D ;W ten sposób za
70 EX AF,AF' ;stępujemy począ-
80 DEC D ;tek procedury
90 DI ;1366, a następ-
l00 LD A,15 ;nie wskakujemy
110 OUT (254),A ;do jej wnętrza:
120 CALL 1378
130 LD A,O ;Czarna ramka oz-
140 JR C,OK ;naczać będzie po-
50 LD A,7 ;prawne wczytanie,
160 OK OUT (254),A ;biała - blędne.
170 CZEKAJ LD A,191 ;Czekamy na wciś-
18O IN A,(254) ;nięcie klawisza
190 RRA ;ENTER.
200 JR C,CZEKAJ
210 LD IX,O ;Nagrywamy wczy-
220 LD DE,dl-16384 ;tany blok kodu
230 LD A,255 ;na taśmę.
240 CALL 1218 ;oraz inicjujemy
250 LD HL,64999 ;działanie sys
260 JP 4633 ;temu BASIC-a.
Zamiast wczytywać asembler i wpisywać powyższą procedurę, możesz uruchomić program z listingu 1.
Jak wygląda cięcie bloku? Ustawiamy taśmę na blo-ku kodu, który chcemy ciąć. Jeśli miał on nagłówek -to pomijamy go. Uruchamiamy procedurę i włączamy magnetofon. Nie przestrasz się, gdy w pewnym mo-mencie ujrzysz, że program wczytuje się na ekran -tak przecież powinno być. Po załadowaniu bloku kolor ramki sygnalizuje poprawność wczytania: jeśli ramka jest czarna, to wszystko jest OK., jeśli biała-wystąpił błąd. Teraz włóż do magnetofonu drugą kasetę, włącz nagrywanie i wciśnij ENTER. Program nagra się na taś-mę, a potem procedura wróci do BASIC-a, inicjując sy-stem komunikatem " 1982...", ale nie kasując pamięci (niszczony jest jedynie obszar od początku ekra-nu, do adresu ok. 24000). Teraz, preparując nagłówek lub pisząc króciutką procedurkę, możesz wczytać uzy-skany blok pod dowolny adres.
Gdy już znajdziesz to, czego szukałeś, i chcesz uru-chomić poprawiony program, musisz się trochę pomę-czyć i "skleić" "rozcięty" program lub napisać do nie-go nową procedurę ładującą. Jeżeli program wypełniał całe 48K pamięci RAM, możliwa jest tylko ta druga me-toda.
Jeśli chcesz skleić bloki -wystarczy napisać proce-durę podobną do rozcinającej, lecz która wczyta pierw-szy blok pod adres 16384, drugi - zaraz za nim, a na-stępnie nagra je razem, jako jeden blok.
W następnym, ostatnim już, odcinku naszego cyklu -odkodowywanie zakodowanych bloków "Bytes".
Robert Dudzik, Tomnasz Surmacz
Od red. W czwartym odcinku naszego cyklu zamienione zostały dwa listingi w tekście - przepraszamy.
Część VII
Cześć po raz ostatni !
W dzisiejszym odcinku mamy zamiar zająć się odkodowywaniem zakodowanych bloków typu "Bytes" (brzmi to trochę jak masło maślane, ale cóż robić?), oraz niepublikowanymi instrukcjami mikroprocesora Z-80. Na zakończenie - garść krótkich i prostych, lecz często używanych za-bezpieczeń.
Na wstępie jednak niewielkie uzupełnienie do poprzedniego odcinka. Opisaliśmy w nim sposób uruchamiania się bloków kodu maszynowego przez wczytanie w obszar stosu ma-szynowego. Aby taki blok się uruchomił, wystarczy jedynie wpisać instrukcję LOAD ""CODE, która go załaduje. Aby przekonać się o tym w praktyce, wpisz i uruchom program z listingu 1.
Nagra on na taśmę krótki blok kodu maszynowego, który będzie się sam uruchamiał. Po nagraniu tego bloku wykasuj pamięć przez RANDOMIZE USR O lub RESET (ewentualnie przestaw RAMTOP na normalną wartość przez CLEAR 65367) i wczytaj go wpisując LOAD ""CODE. Jego zadaniem jest poinformowanie o tym, że się uruchomił- robi to wysyłając kilka dźwięków. Usłyszysz je natychmiast po wczytaniu programu, gdy tylko z ramki znikną granatowo-żółte paski. To tyle tytułem uzupełnienia.
Co to znaczy, że program lub jego blok jest zakodowany? Zakodowanie jest to rodzaj ba-rdzo prostego szyfru uniemożliwiającego poprawne działanie programu Do jego urucho-mienia służy specjalna procedura odkodowująca, która jest umieszczona w tym programie oraz - co najważniejsze i chyba oczywiste - nie jest zakodowana. Czemu więc służy kodowanie? Jest to po prostu utrudnianie dostępu do tekstu programu wtedy, gdy złamane zostały wcześniejsze zabezpieczenia i program jest wczytany bez uruchomienia, bowiem to, co znajduje się w pamięci nie jest programem, lecz jego "półproduktem", który dopiero ' po przetworzeniu przez procedurę odkodowującą staje się właściwym programem. Może to polegać np. na zanegowaniu wszystkich bajtów programu, a więc-jeśli w grze będzie-my szukać disasemblerem kodu instrukcji DEC A lub podobnej, aby znaleźć "nieśmiertel-ność", to nasze poszukiwania spełzną na niczym, bo instrukcja ta zamiast kodu 3Dh bę-dzie reprezentowana przez kod C2h. Dobrym przykładem jest znany wam zapewne pro-gram "Art Studio". Jego część BASIC-owa nie jest praktycznie w żaden sposób zabezpie-czona, lecz główny blok programu ("studio-mc" CODE 26000, 30672) jest właśnie częś-ciowo zakodowany. Co zrobić, aby go odkodować? - po prostu znaleźć adres, od jakiego blok ten jest uruchamiany. W przypadku "Art Studia" jest to adres 26000. Tam znajduje się instrukcja JP 26024, która skacze do procedury odkodowującej. Oto jej tekst:

Najpierw procedura umieszcza na stosie adres 26049. Wrócimy zaraz do tego. Teraz za-czyna się właściwe odkodowywanie: do rejestru HL ponownie ładowany jest adres 26049 - jako początek zakodowanego bloku, do DE - 27719 jako adres ostatniego zakodowa-nego bajtu. Następnie w pętli odkodowywane są kolejne bajty - instrukcje SUB 34, RLCA i XOR #CC są kluczem, według którego odszyfrowywana jest ta część programu. Wresz-cie - sprawdzany jest warunek osiągnięcia adresu 27719 (zawartego w DE), który kończy odkodowywanie. Wykonywana jest instrukcja RET, ale ostatnim zapisanym na stosie adre-sem nie jest adres powrotu do BASIC-a, lecz zapisany na początku przez PUSH HL adres 26049, czyli adres dopiero co odkodowanego bloku, następuje więc jego uruchomienie.
Zwróćcie uwagę na to, jakie instrukcje dokonują deszyfracji; żadna z nich nie gubi ani je-dnego bitu. Odejmowanie przeprowadzane jest modulo 256 i dla dwóch różnych danych wejściowych wyniki też muszą być różne, RLCA zamienia wartości bitów 7., 6., 3., i 2. na przeciwne. Innymi instrukcjami mającymi tę własność są np. ADD, INC, DEC, RRCA, NEG, CPL ale nie OR ani AND. W jaki sposób więc należy odkodować ten blok? Najprościej - wpisując w BASIC-u:
POKE 26027,0 (kod instrukcji NOP)
likwidując tym samym instrukcję PUSH HL (RET na końcu procedury powróci do BASICA) i wykonać RANDOMIZE USR 26000. Warto jednak pamiętać o tym, że procedura odkodo-wująca może być sprawdzana przez inny fragment programu. W "Art Studiu" tak istotnie jest. Oto dalsza część programu:
Fragment ten sprawdza, czy procedura odkodowująca na pewno uruchomiła cały program, a jeśli nie - przez RET NZ kasuje pamięć (bo na stosie zapisany jest adres 0).
Cóż więc robić? Można kolejno likwidować te zabezpieczenia, ale w niektórych progra-mach jest to bezcelowe. Wtedy pozostaje inne wyjście: ponownie zakodować program, tzn. postąpić dokładnie odwrotnie niż procedura odkodowująca. W naszym przypadku na-leżałoby wykonywać:
XOR #CC RRCA ADD 34
W "Art Studiu", nie wiadomo po co, została umieszczona procedura kodująca. Znajduje się ona pod adresem 26003, a wygląda w ten sposób:
Jak widać - jest ona zbudowana analogicznie, jak procedura odkodowująca. Jeśli takiej procedury nie ma - można ją szybko i prosto napisać na podstawie procedury odkodowu-jącej (np. ATIC ATAC, NIGHT SHADE, THE LAST WORD).
W zabezpieczeniach programów stosowane są także mało znane, a nie publikowane w katalogach firmowych rozkazy mikroprocesora Z-80. Dzięki ich zastosowaniu działanie pro-gramu staje się mało czytelne, a i oglądanie go disasemblerem jest utrudnione.
Zacznijmy od początku. Najczęściej spotykanym rodzajem instrukcji niepublikowanych są rozkazy operujące na połówkach rejestrów indeksowych IX i IY w grupie rozkazów nie poprzedzonych żadnym innym prefiksem (tzn. CBh lub EDh). Polegają one na prefiksowa-niu kodem DDh lub FDh rozkazu dotyczącego rejestru H lub L. Wówczas zamiast tego re-jestru brana jest odpowiednia połówka rejestru indeksowego. Przez hX oznacza się zwykle starszą część rejestru IX, przez IX - młodszą. Analogicznie hY i IY. Oto przykłady:
Prawdziwe jest to dla wszystkich rozkazów przestań jednobajtowych między rejestrami i ośmiobitowych operacji AND, OR, XOR, ADD, ADC, SUB, SBC i CP - wykonywanych w akumulatorze.
Prefiks FDh lub DDh dotyczy wszystkich rejestrów H, L lub HL występujących w rozka-zie, a więc nie jest możliwe użycie w jednej instrukcji komórki adresowanej przez (HL), re-jestru HL, H lub L równocześnie z hX, hY, IX, IY, IX lub IY, (w dalszej części ograniczymy się do rejestru IX, choć wszystko to dotyczy także rejestru IY) np.
Nieco inaczej przedstawia się sprawa rotacji komórki adresowanej przez rejestr indekso-wy, tzn. instrukcji rozpoczynających się kodem DDCB. Instrukcje typu RR (IX+dd) itp. są szczegółowo opisane we wszelkich dostępnych materiałach o mikroprocesorze Z-80, mało kto wie jednak o instrukcjach typu RR (IX+dd),r itp., gdzie r oznacza dowolny rejestr mi-kroprocesora. Polegają one na prefiksowaniu kodem DDh lub FDh instrukcji typu RR r. Po-dobnie rzecz się ma z instrukcjami SET n,(IX+d),r oraz RES n,(IX+d),r (ale dla BIT-już nie). Działanie takiej instrukcji polega na wykonaniu "normalnego" rozkazu RR (IX-t-d) (lub podobnego), SET n,(IX+d) lub RES n,(IX+d) a następnie przesłaniu wyniku zarówno do komórki (IX+d) jak i do odpowiedniego rejestru wewnętrznego mikroprocesora. Np.:
CB13 RL E
DDCB0113 RL(IX+1),E
powoduje rotację komórki o adresie IX+1 w lewo 0 1 bit i odesłanie tego wyniku do rejestru E, co w "konwencjonalny" sposób należałoby zrobić:
DDCB0116 RL(IX+1)
DD5E01 LD E,(IX+1)
Na zakończenie omawiania tego typu instrukcji należy jeszcze przypomnieć, że nie ma roz-kazu EX DE,IX czy EX DE,IY. Prefiksowanie rozkazu EX DE,HL nie daje żadnego skutku. Podobnie - prefiksowanie rozkazów, których kody rozpoczynają się od EDh oraz tych, w których nie występuje żaden z rejestrów H, L lub pary HL (np. LD B,n, RRCA itp.).
Kolejnym ciekawym rozkazem jest SLI (Shift Left and Increment), którego działanie jest analogiczne do SLA, z tą różnicą, że najmłodszy bit jest ustawiany na 1. Zachowanie wska-źników jest identyczne, jak przy SLA lub innych obrotach:
Czasem nieco problemów stwarza budowa rejestru flagowego, szczególnie wtedy, gdy wykorzystywany jest on w dość nietypowy sposób, np.:
PUSH AF
POP BC
RL C
JR NC,...
Jego budowa jest przedstawiona na rys. 1. Dodatkową własnością rejestru F jest to, że bity 3. i 5. (oznaczone jako F3 i F5) dokładnie odzwierciedlają stan 3. i 5. bitu wyniku ostatnio wykonywanej ośmiobitowej operacji arytmetycznej lub logicznej albo operacji IN r,(C) i IN F,(C). Np. po wykonaniu:
XOR A ;zapisanie wartości 0 ADD A,15 ;wynik-%00001111 wskaźnik F5 zostanie skasowany, F3 - ustawiony.
Ostatnią, chyba ostatnią tajemnicą Z-80 (któż zaręczy że nie ma ich więcej?) jest rejestr odświeżania pamięci R, a dokładniej -to, że gdy po kolejnym cyklu maszynowym mikro-procesora inkrementowana jest wartość tego rejestru, to jego najstarszy bit pozostaje nie-naruszony, może być więc wykorzystany do pamiętania dowolnej, oczywiście jednobito-wej, informacji. Ważne jest także to, że instrukcja LD A,R, za pomocą której może być odzyskana ta informacja, ustawia również wskaźnik S, a więc nie jest potrzebna dodatkowa instrukcja sprawdzająca wartość tego bitu.
Na zakończenie kilka krótkich, ale dość często używanych zabezpieczeń. Ich likwidowa-nie polega zwykle na postępowaniu odwrotnym do zabezpieczenia. Najczęściej pod tym względem eksploatowane są zmienne systemowe. Np. zmienna DFSZ (23659) określa ilość linii w dolnej części ekranu (potrzebną do wydrukowania komunikatu o błędzie lub do wprowadzania danych). Podczas działania programu wartość ta wynosi zwykle 2, lecz wy-starczy wpisać tam wartość 0, by program zawiesił się przy próbie przerwania (bo drukowa-ny jest komunikat, na który nie ma miejsca). Jeżeli w programie znajduje się instrukcja IN-PUT lub CLS, to ma ona podobny skutek.
Dość częstym zabezpieczeniem, używanym nawet w bardzo poważnych programach, jest zmiana wartości zmiennej BORDCR (23624), która określa kolor ramki i atrybutów dolnej części ekranu. Znaczenie poszczególnych bitów przedstawia rys. 2. Zabezpieczenie polega na tym, że kolor atramentu jest taki sam, jak ramki, a więc po zatrzymaniu programu możemy odnieść wrażenie, że program się zawiesił (nie widać komunikatu). Odbezpiecze-nie tego jest wręcz trywialne - wystarczy wpisać BORDER 0 (lub inny kolor).
Kolejnym prostym zabezpieczeniem jest zmiana wartości zmiennej ERRSP (23613-14) lub dna stosu maszynowego (zmienna ta wskazuje właśnie dno stosu) gdzie znajduje się normalnie adres procedury obsługującej błąd BASIC-a (wywoływanej przez RST 8). Zmniejszenie wartości ERRSP o 2 powoduje zabezpieczenie programu przed BREAK i każdym innym błędem - program ponownie się uruchamia od miejsca, w którym wystąpił błąd. Zmiana zawartości dna stosu maszynowego lab inna zmiana wartości ERRSP może powodować zawieszanie się bądź "resetowanie" komputera w wypadku błędu.
Znanym zabezpieczeniem jest również wpisanie wartości większych od 9999 do komó-rek pamięci oznaczających numer linii BASIC-a (Pierwszy bajt starszy!). Gdy mieści się on w granicach 10000 - 16383, na listingu wygląda to np. :000 (zamiast 10000) i linie te nie dają się poprawić (EDIT), jeśli natomiast przekracza 16384-dalsza część programu uzna-wana jest za nieistniejącą. Można też spotkać zabezpieczenie polegające na wpisywaniu minimalnych wartości (tzn. 1) do zmiennych REPDEL (23561) i REPPER (23562) co w efekcie utrudnia pracę z komputerem, ale do jego zlikwidowania potrzebny jest tylko szybki refleks (wpisz przez POKE-i normalne wartości: 35 i 5).
Warto jeszcze wspomnieć o zmiennej NMIADD, która nie jest używana ze względu na błąd w ROM-ie. Miała ona zawierać adres obsługi przerwania NMI (przyjmowanego za-wsze!), błąd polega na tym, że mikroprocesor skacze tam tylko wtedy, gdy wynosi on 0. Gdyby jednak dorobić do Spectrum odpowiednią przystawkę z pamięcią EPROM (np. Baj-tek 7/87), droga do zatrzymania dowolnego programu stałaby otworem
I to już wszystko. Jeżeli zainteresował cię ten temat, a masz jakiś ciekawy problem zwią-zany z odbezpieczaniem programów, napisz do redakcji (zaznaczając na kopercie "Od środka"). Być może problemów tych jest jeszcze na tyle dużo, że powstanie dodatkowy odcinek tego cyklu.
Tomasz Surmacz Robert Dudzik
Powrót do strony głównej...