Bölüm 4
"Beach Head"
Haziran, 1985.
Karne almamıza birkaç hafta kala havalar güzelleşmiş, kravatlar iyice gevşemişti. Öğle tatilinde arkadaşlarım tribünde oturup okulumuzda düzenlenen liseler arası hentbol turnuva maçlarını keyifle izlerken, ben de kucağıma Melbourne House’un kitabını alıp bir yandan güneşlenir bir yandan da dikkatle okurdum. O güzel günlerden biriydi… Evde test etmek üzere, İngilizce gramer kitabımın kenarına minik bir kod yazdım. Tamamen ROM çağrıları kullanan, "her işi kitabına göre yapan" kısacık bir routine’di bu.
org &8000
ld ix,&4000
ld de,17
xor a
scf
call &0556
ld a,255
ld (&400e),a
ld ix,&4000
ld hl,(&5c53)
jp &0873
İlk iş olarak 17 byte uzunluğundaki header’ı ekran hafızasının en başına (&4000 adresine) sanki headerless bir dosyaymış gibi yüklüyor, ardından header’ın 13. ve 14. byte’larında belirtilen BASIC’te sıçranacak olan satır sayısının sadece ikinci byte’ını header’ın içinden siliyor, bir sonraki block’u yükleyebilmek için header’ın bulunduğu adresi ve block’un nereye yükleneceğini gösteren pointer’ı sistem değişkeni PROG (&5C53) ile belirtiyor, son olarak da ROM’dan LD-PROG (&0873) subroutine’ini çağırarak ilk bloğu yüklüyordum. Teoride, bu block yüklenecek ama auto-run etmeyecekti, OK diyerek BASIC’e geri dönecekti.
Öyle de oldu!
RANDOMIZE USR 32768 komutuyla yazdığım kodu çağırdım. Teybe
Match Day kasetini koyup, Play’e bastım. Önce header yüklendi, ardından ilk block… Sonra da ekrana OK yazısı çıkıp BASIC’e geri döndü. Yazdığım kod çalışmıştı! BASIC ve ULA’nın kullandığı "paylaşımlı" alanda olduğumu bildiğim için RANDOMIZE USR 24832 komutuyla &6100 adresine sıçramayı aklımdan bile geçirmedim. Kodun crash edeceğinden adım gibi emindim. Zira, BASIC’e geri döndüğüm için BASIC buffer değerlerinin yüklediğim kodun üzerine yerleştiğini artık bilecek kadar tecrübeliydim.
Auto-run’ı kontrol etmek amacıyla BASIC satır sayısını hacklediğim bölümü kodun içerisinden sildim. Eğer her şey yolunda giderse, işlevsel olarak ZX Spectrum’un LOAD "" komutuna denk bir kod yazmış olacaktım.
org &8000
ld ix,&4000
ld de,17
xor a
scf
call &0556
ld ix,&4000
ld hl,(&5c53)
jp &0873
Kaseti başa sarıp, RANDOMIZE USR 32768 komutuyla yazdığım kodu çağırdım. İlk header ve block yüklendi. Ardından border yanıp sönmeye başladı. Turbo loader çalışmıştı! Yüklemek için kasetten sinyal bekliyordu…
Müjdeyi vermek için hemen ECOM’a koştum. Mahir Bey yerinde yoktu. Ben de müjdemi Necati Ağabey ile paylaştım.
"Kırdın yani oyunu!" dedi. Ben de
"Yok, henüz o aşamaya gelemedim. Sadece turbo loader’ı kırdım" dedim. Yaptıklarımı dükkanda kurulu olan Spectrum üzerinde çalıştırarak tüm detaylarıyla anlattım. Necati Ağabey büyük bir keyifle,
"Darısı oyunun başına!" dedi. Gülüştük.
Bu macera sonucunda, Spectrum’un tüm I/O işlemlerini ve BASIC komutlarının ROM tarafından nasıl interpret edildiğini yalayıp yutmuştum. BASIC’in auto-run özelliğinin nasıl açıp kapanacağından tutun da, header’ın hangi adrese nasıl yükleneceğine varana kadar epey tecrübe edinmiştim. Hepsinden daha önemlisi, bir önceki yöntemde neyi eksik yaptığımı anlamıştım! Çok basit ve bir o kadar da can alıcı noktayı gözden kaçırmış, ilk bloğu headerless olarak yükleyebilmek için asıl header’ı atlamıştım! Match Day loader &6100’e sıçradıktan sonra sadece register’ların o anki değerlerine bakmakla kalmıyor, header ile birlikte yüklenen dosya tipi (1 byte), dosya adı (10 byte), block uzunluğu (2 byte), BASIC başlangıç satırı (2 byte) gibi verileri de kontrol ediyordu. Bir önceki yöntemde gözardı ettiğim 17 byte (header) bana pahalıya mal olmuştu. İşi kitabına göre yapıp, tüm yükleme işlemlerini ROM üzerinden doğru sırayla çağırmam ise bu sorunu doğal olarak ortadan kaldırmıştı.
Artık bir sonraki aşamaya geçip, &5C00 adresine decode edilen kodu kaldığım yerden incelemeye devam edebilirdim. İlk dikkatimi çeken şey, stack pointer orijinal değerinin değiştirilerek &5C00 öncesi bir adrese çekiliyor olmasıydı. Dikkatimi çeken ikinci şey ise, contended alandan daha ileriye taşınacak olan kodun oldukça karmaşık olmasıydı. Acaba taşıma sırasında yeni bir decoding mi yapılıyordu? Uzun süre kodun içinde çırpındıktan sonra, hem müjdemi vermek hem de geldiğim yeni noktayla ilgili görüşlerini almak için Mahir Bey’e danışmaya karar verdim. Takıldığım yerleri kendisine gösterince,
"Haklısın, oldukça karışık" dedi. Ve, o ana kadar duyduğum en mükemmel fikri ortaya attı:
— Madem o kodun içinden çıkamıyorsun, ona benzer başka bir kod bulup karşılaştırma imkanın var mı? Aralarındaki ortak ve farklı yerlere bakıp belki bir şeyler sezebilirsin.Harika bir tavsiyeydi bu! Hemen eve gidip
Match Day’in loader’ına benzeyen, pilot sinyal sırasında "dıt-dıt-dıt" diye kesik kesik ton üreten
Daley Thompson’s Decathlon’u denedim. Her ne kadar pilot sinyal benzese de, loading screen bitiminde sinyal kesilmiyor, oyun tek parça halinde yüklemeye devam ediyordu. Belli ki bu oyunu kırmak
Match Day’den çok daha zordu. Bunun üzerine elimdeki diğer oyunları tek tek incelemeye başladım. Maalesef elimdeki oyunların pilot sinyallerinde o kesik tonlama yoktu.
Mahir Bey’e durumu anlattığımda,
"Öyleyse sana bol miktarda turbo loader’lı oyun bulmak lazım. Ama crack versiyon olmamalı, orijinal kopya olmalı" dedi. Haklıydı. Günlerce piyasayı araştırdım. Gitmediğim yer, girmediğim dükkan kalmadı. Sonunda Elmadağ’da, Şan Tiyatrosu’nu biraz geçtikten sonra solda, çok şık bir mağaza içinde bende olmayan bir sürü Spectrum oyunu buldum. Mağazanın satış görevlisi, büyük bir gururla,
"Biz bunları yurtdışından orijinal getirip kendi sistemlerimizle burada çoğaltıyoruz" dedi. Birkaç hafta boyunca tüm harçlığımı bu mağazadaki Spectrum oyunlarına yatırdım dersem, yanlış olmaz. Yol masrafı da cabası.
Uzun arayışlar sonucu, duymak istediğim pilot sinyalinin benzerini nihayet üzerinde
Beach Head yazan kasette duydum. Kesinlikle aynı tondu bu! Hemen loader’ı disassemble ettim. Farklı adreste yine aynı
"Attempting to crack SPEEDLOCK…" mesajını görünce, çok sevindim.
Match Day için yaptığım tüm işlemleri ona da uyguladım. Daha sonra her iki kodun yazıcı çıktısını almak üzere ECOM’a gittim. Necati Ağabey’den ücreti karşılığında elimdeki iki kodu yazdırmasını rica ettim. Gülümseyerek kodları özenle bastı,
"Doğru yoldasın. Sen şimdi eve git, işine odaklan. Sonra ödersin." dedi. Çok mutlu oldum, biraz da utandım doğrusu.
İki loader’ın çıktısını yan yana koyunca, benzer noktalar hemen ortaya çıktı. Neyin data, neyin kod olduğu artık biraz daha netti. Hâlâ anlayamadığım yerler vardı, ama ben anladığım yerlere odaklanmayı tercih ettim.
Öncelikle, elimde olan verileri not aldım:
- Her iki loader da kesinlikle ROM kullanmıyor, tüm interruptları kapatıyor ve kasetten okuma işlemleri için portlara erişerek kendine ait “custom” yükleme routine’lerini kullanıyordu.
- Her header yükleme öncesi dıt-dıt-dıt şeklinde gelen pilot sinyal aralıkları, büyük olasılıkla okuma hızıyla ilgili aralıkları ölçmek için kullanılıyordu. Bir bakıma, kasetin devir hızına göre kod kendi içinde küçük bir kalibrasyon yapmaya çalışıyor olabilirdi.
- Duyabildiğim kadarıyla, oyunların kasetten okuma hızı Spectrum standardı olan (ortalama) 1.365 baud’dan çok daha fazlaydı. Bu da, her dıt aralığı Spectrum’un ROM’unda yer alan yükleme kodundaki 358T state bekleme süresinden çok daha az olması anlamına geliyordu. Daha az bekleme, daha yüksek okuma hızı… Ancak bundan emin olabilmek için custom yükleme koduna erişebilmem gerekiyordu.
- Loader içerisine paketlenmiş olan asıl yükleme routine’i, henüz anlayamadığım bir şekilde encrypt edilmişti. Ya sıkıştırılmış, ya encode edilmiş, ya da her ikisi birden uygulanmıştı. Üstelik, bu işlem dizisi birden fazla kez uygulanıyor olabilirdi.
Bunları bilerek, kendime bir yol haritası çıkarttım:
- Her iki loader’ın ortak noktaları nelerdir?
- Encrypt edilmiş datayı nasıl decrypt edebilirim?
- Custom loader hangi adreste duruyor?
- Custom loader nasıl çalışıyor?
- Custom loader’ın içine, oyunu standart hızda kaydetmemi sağlayacak şekilde kendi kodumu nasıl ekleyebilirim?
Hem
Match Day hem de
Beach Head loader’larında ortak olan, taşıma işlemini gerçekleştirdiğinden ya da taşınan kod olduğundan şüphelendiğim yerden yola çıktım.
- Match Day - | - Beach Head -
|
5f04: dec sp | 5feb: xor iyl
5f05: dec sp | 5fed: ld iyh,&c8
5f06: ld iyh,&ec | 5ff0: ld iyl,&20
5f09: ld iyl,&04 | 5ff3: dec sp
5f0c: ex (sp),iy | 5ff4: dec sp
5f0e: ld bc,&002e | 5ff5: ld bc,&fe42
5f11: add iy,bc | 5ff8: ex (sp),iy
5f13: ld e,iyl | 5ffa: ld hl,&c6aa
5f15: ld d,iyh | 5ffd: add iy,bc
5f17: ld l,e | 5fff: ld bc,&01be
5f18: ld h,d | 6002: ld e,iyl
5f19: ld bc,&01d5 | 6004: ld d,iyh
5f1c: ld a,i | 6006: ex de, hl
5f1e: call po,&3008 | 6007: xor (hl)
5f21: ld a,r | 6008: ld (de),a
5f23: xor (hl) | 6009: ld a,(hl)
5f24: ld (hl),a | 600a: inc hl
5f25: ldi | 600b: inc de
5f27: ret po | 600c: dec bc
5f28: dec sp | 600d: ld iyl,a
5f29: dec sp | 600f: ld a,b
5f2a: ret pe | 6010: or c
5f2b: ... | 6011: ld a,iyl
.. | 6013: jr nz,&6007
. | 6015: ld iy,&5c3a
| 6019: ret
Adres olabilecek değerleri tek tek hafızada takip etmeye başladım. Kısa süre içinde
Match Day’in loader kodunun &EC04 adresine,
Beach Head’inkinin ise &C820 adresine sıçradığını fark ettim.
- Match Day - | - Beach Head -
|
ec04: ld sp,&5bff | c820: ld sp,&609e
ec07: ld ix,&8000 | c823: ld ix,&8000
ec0b: ld de,&0014 | c827: ld de,&0014
ec0e: call &eb92 | c82a: call &c7ae
ec11: call &ebc0 | c82d: call &c7dc
ec14: ld ix,&4000 | c830: ld ix,&4000
ec18: ld de,&1b00 | c834: ld de,&1b00
ec1b: call &eb92 | c837: call &c7ae
ec1e: call &ebc0 | c83a: call &c7dc
ec21: ld ix,&5dc0 | c83d: ld ix,&60a0
ec25: ld de,&8ca0 | c841: ld de,&6609
ec28: call &eaa9 | c844: call &c6c5
ec2b: ld ix,&ec86 | c847: ld ix,&c8aa
ec2f: ld de,&1379 | c84b: ld de,&3750
ec32: call &eaa9 | c84e: call &c6c5
ec35: ld a,(&ec03) | c851: ld a,(&c81f)
ec38: cp &00 | c854: cp &00
ec3a: jp nz,&0000 | c856: jp nz,&0000
ec3d: ld hl,&ec4b | c859: ld de,&c6aa
ec40: ld de,&5b00 | c85c: ld bc,&0176
ec43: ld bc,&0018 | c85f: ld hl,&053f
ec46: ldir | c862: ldir
ec48: jp &5b00 | c864: ei
ec4b: ld hl,&5dc0 | c865: jp &8000
ec4e: ld de,&ea60 |
ec51: ld bc,&0226 |
ec54: ldir |
ec56: ld sp,&60ff |
ec59: ld iy,&5c3a |
ec5d: im 1 |
ec5f: ei |
ec60: jp &ffbc |
Artık Speedlock’ın custom yükleme routine’lerine yapılan tüm CALL’lar karşımda apaçık duruyordu. Header ve Loading Screen için aynı 2 adres çağırılırken, data block’lar için farklı adresler çağırılıyordu. Tüm işlemler bittikten sonra da, son satırdaki JP komutuyla oyun başlatılıyordu. Bu noktadan sonra oyunu kırmak artık çocuk oyuncağıydı!
Hemen minik bir headerless dosya kaydetme routine’i yazdım, hafızanın kullanılmayan bir köşesine yerleştirdim. Sondaki JP komutu yerine yazdığım bu routine’i çağırarak, önce belleği 16K’lık parçalar halinde kasete dump ettim, ardından da hepsini microdrive’a aktardım. Dump ettiğim dosyaları temizleme ve kısaltma işlemlerinin ardından, oyunun ilk bloğu kasetten yüklendikten sonra ekrana
"cracked by matahari" yazacak minik bir loader kodladım. Ertesi gün hem
Match Day’i hem de
Beach Head’i kırık halde Necati Ağabey’e teslim ettim, sonra da Mahir Bey’in yanına koştum.
— Şimdi ne yapacaksın Mert?
— Turbo loader kodunu incelemeye devam edeceğim, efendim. Custom loader’ı en son bit’ine kadar takip edeceğim. Hepsini okuyup anlayacağım. Belki bir gün ben de kendi turbo loader’ımı yazarım!
— Aferin! Ben de senden bunu beklerdim. Kodu incelemeye, öğrenmeye devam et. Asıl bilgi orada…Doğru yolda olduğumun onayını almanın mutluluğu ile hemen eve gittim, turbo loader kodlarını okumaya devam ettim. Artık tüm okuma işlemlerini "karşılaştırmalı" olarak yürütüyordum.
- Match Day - | - Beach Head -
|
ec04: ld sp,&5bff | c820: ld sp,&609e
ec07: ld ix,&8000 | c823: ld ix,&8000
ec0b: ld de,&0014 | c827: ld de,&0014
ec0e: call &eb92 | c82a: call &c7ae
ec11: call &ebc0 | c82d: call &c7dc
ec14: ... | c830: ...
.. ..
. .
Speedlock’ın kullandığı header yapısı, Spectrum’un klasik "marker byte + 17 byte + checksum byte" formatından farklıydı. Her iki oyun da ekran bloğu öncesi toplam 20 (&14) byte header yüklüyordu. İlginç olan, bu uzunluğun kasetteki uzunlukla uyuşmamasıydı.
Beach Head kasetinde loading screen header’ının uzunluğu 22 byte iken,
Match Day kasetindeki 21 byte uzunluğundaydı. Bir diğer deyişle, her iki oyun aynı miktarda (20 byte) header datasını kasetten yüklese de, kasette kayıtlı olan header uzunlukları talep edilenden biraz daha fazlaydı. Elbette bu da kafa karıştırmak için konulmuş bir tuzaktı!
Keyifle kod okuma işlemine devam ettim... Header ve Loading Screen için çağrılan komutlardan ilkini seçtim:
Match Day için call &EB92,
Beach Head için call &C7AE. İki farklı oyun iki farklı adres olmasına karşın, çağrılan subroutine’ler instruction bazında birebir aynıydı!
- Match Day - | - Beach Head -
|
eb92: call &eaa9 | c7ae: call &c6c5
eb95: ld hl,&8000 | c7b1: ld hl,&8000
eb98: ld b,&ff | c7b4: ld b,&ff
eb9a: push bc | c7b6: push bc
eb9b: call &eba4 | c7b7: call &c7c0
eb9e: ld (hl),e | c7ba: ld (hl),e
eb9f: inc hl | c7bb: inc hl
eba0: pop bc | c7bc: pop bc
eba1: djnz &eb9a | c7bd: djnz &c7b6
eba3: ret | c7bf: ret
eba4: ... | c7c0: ...
.. | ..
. | .
Bu subroutine’ler, hemen başka bir subroutine’i çağırarak kod içerisinde dallanmaya başlıyordu.
- Match Day - | - Beach Head -
|
eaa9: di | c6c5: di
eaaa: inc d | c6c6: inc d
eaab: dec d | c6c7: dec d
eaac: ld a,&0f | c6c8: ld a,&0f
eaae: out (&fe),a | c6ca: out (&fe),a
eab0: ld hl,&eb84 | c6cc: ld hl,&c7a0
eab3: push hl | c6cf: push hl
eab4: in a,(&fe) | c6d0: in a,(&fe)
eab6: rra | c6d2: rra
eab7: and &20 | c6d3: and &20
eab9: or &02 | c6d5: or &02
eabb: ld c,a | c6d7: ld c,a
eabc: cp a | c6d8: cp a
eabd: call &ea8e | c6d9: call &c6aa
eac0: jr nc,&eabd | c6dc: jr nc,&c6d9
eac2: ... | c6de: ...
.. | ..
. | .
Son geldiğim nokta, &FE port’undan low-level yükleme işlemlerini gerçekleştiren subroutine’lerdi.
- Match Day - | - Beach Head -
|
ea8e: dec a | c6aa: dec a
ea8f: jr nz,&ea8e. | c6ab: jr nz,&c6aa
ea91: and a | c6ad: and a
ea92: inc b | c6ae: inc b
ea93: ret z | c6af: ret z
ea94: ld a,&7f | c6b0: ld a,&7f
ea96: in a,(&fe) | c6b2: in a,(&fe)
ea98: rra | c6b4: rra
ea99: xor c | c6b5: xor c
ea9a: and &20 | c6b6: and &20
ea9c: jr z,&ea92 | c6b8: jr z,&ea92
ea9e: ld a,c | c6ba: ld a,c
ea9f: cpl | c6bb: cpl
eaa0: ld c,a | c6bc: ld c,a
eaa1: and &07 | c6bd: and &07
eaa3: or &08 | c6bf: or &08
eaa5: out (&fe),a | c6c1: out (&fe),a
eaa7: scf | c6c3: scf
eaa8: ret | c6c4: ret
Bu kodları gördüğüm an,
"Aaaaa, ben bunları bir yerden tanıyorum!" dedim. Hemen Melbourne House’un ROM kitabının 20. sayfasını açıp baktım. Yanılmamıştım, bildiğimiz LD-EDGE-1 (&05E7) subroutine’in kopyasıydı bu! Tek fark, LD-EDGE-2 (&05E3) subroutine’indeki gibi ezbere harcanan 358T state yoktu. Kasetteki sinyal aralığı örneklenerek elde edilen kalibrasyon değeri Accumulator içerisine yükleniyor, bu "dinamik" değer sıfırlanana kadar bekleniyordu. Ardından port I/O işlemine geçiliyor, işlem sonunda elde edilen polariteye göre border rengi belirlenip aynı port üzerinden ekrana gönderiliyordu. İşlem sırası ve ardarda gelen 19 adet instruction, (burada kullanılmayan RET NC komutu dışında) ZX Spectrum’un ROM’undaki LD-DELAY (&05E9) subroutine’iyle birebir aynıydı!
Uzun uğraşlar, uzun günler, uzun geceler sonunda vardığım yer, Spectrum’un ROM’undaki kasetten yükleme routine’lerinden fazlasıyla esinlenerek (!) yazılan "basit" bir turbo loader’dı. Onca şifreleme çabası ve kafa karıştırıcı kod okuma yolculuğundan sonra, dönüp dolaşıp yine Spectrum’un ROM’undaki standart port okuma ve bit ayıklama işlemlerine geri gelmiştim. Aradaki tek fark, 14 yaşında başlayıp bir sene içinde tamamladığım bu sıradışı yolculuk sırasında edindiğim bilgi ve tecrübeydi. Bundan böyle; problem çözme, bilimsel yöntem, eleştirel düşünme, karar verme, sorgulama, karşılaştırmalı düşünme, vb. önemli kavramlar hayatımın ayrılmaz bir parçası olacaktı. Bu değerler ile tanışmama yardımcı olan, iyi bir yazılımcı olabilmem için usta-çırak ilişkisiyle bilgilendiren,
"yapamazsın, kıramazsın, seni aşar" diyenlerin aksine cesaretlendiren, hem güzel ahlakı hem de üst düzey mühendislik bilgisiyle aydınlık saçan rahmetli Mahir Bey’in anısı önünde saygıyla eğiliyor, benim için adeta bir okul olan ECOM Mühendislik bünyesinde -
başta Necati Ağabey ve Suna Abla olmak üzere- üzerimde emeği bulunan herkese şükranlarımı sunuyor, artık aramızda olamasalar da farklı bir boyutta ısrarla karşılıklı tavla oynamayı sürdürdüklerini düşündüğüm Ted amcamın ve sevgili babamın ışıklar içinde uyumasını diliyorum.
Sinclair ZX Spectrum’da
Match Day ile başlayıp
Glass ile tamamlanan oyun kırma (cracking) serüvenim, bu iki oyun arasındaki sayısız turbo loader ve oyun boyunca sürdü. O dönemde elde ettiğim kodlama bilgisi ve tecrübesi, 1985 yılından itibaren dönemin en iyi İngiliz ve Fransız oyun firmalarına freelance olarak hizmet vermemin kapılarını açtı. O günden beri aynı tutku, azim ve disiplinle çalışarak küresel oyun sektörüne profesyonel olarak hizmet vermeyi sürdürüyor, soluklanmaksızın yeni bilgilere yelken açabilmek amacıyla ısrarla oyun kırmaya ve reverse engineering yapmaya devam ediyorum.
Doğumunun 40. yılında, ufkumu açarak bugünlere gelebilmemi sağlayan Sinclair ZX Spectrum’u sevgi ve özlemle anıyorum.
* Ekli Dosyalar: