--Willy Byte in the Digital Dimension-- A 4am crack 2021-08-04 --------------------------------------- Name: Willy Byte in the Digital Dimension Genre: action Year: 1984 Credits: Murray Krehbiel (programming) Greg Hammond (graphics) Publisher: Data Trek Platform: Apple ][ (48K) Media: 5.25-inch disk Sides: 2 OS: custom _____________________________________ { } { McCoy: Lieutenant, you are looking } { at the only Starfleet cadet who } { ever beat the no-win scenario. } { Saavik: How? } { Kirk: I reprogrammed the simulation } { so it was possible to rescue the } { ship. } { Saavik: What? } { David: He cheated. } { Kirk: I changed the conditions of } { the test. } { - "Star Trek II" } {_____________________________________} ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways The game boots side B, so we will start there. COPYA no errors on side B read error on first pass of side A Locksmith Fast Disk Backup track 6 of side A is unreadable EDD 4 bit copy (no sync, no count) no errors, but copy reboots just before level 1 (after entering a word or selecting a difficulty level for the random word) Copy ][+ nibble editor track 6 of side A looks normal at first glace --v-- COPY ][ PLUS BIT COPY PROGRAM 8.4 (C) 1982-9 CENTRAL POINT SOFTWARE, INC. ---------------------------------------- TRACK: 06 START: 1FDC LENGTH: 17B7 1FB8: FF FF FF FF FF FF FF FF VIEW 1FC0: FF FF FF FF FF FF FF FF 1FC8: FF FF FF FF FF FF FF FF 1FD0: FF FF FF FF FF FF FF FF 1FD8: FF FF FF FF D5 AA 96 FF <-1FDC 1FE0: FE AA AF AA AA FF FB DE 1FE8: AA EB FF FF FF FF FF FF 1FF0: D5 AA AD 96 96 96 96 96 1FF8: 96 96 96 96 96 96 96 96 --^-- Ah! But look closer at that address field: --v-- 1FD8: FF FF FF FF D5 AA 96 FF <-1FDC ^^^^^^^^ prologue 1FE0: FE AA AF AA AA FF FB DE ^^^^^ ^^^^^ ^^^^^ track sectr chksm --^-- Decoding the 4-and-4 encoded values, this is sector 0 on track...5? But this is track 6! The address field is lying to me. Bad disk, no biscuit! However, this can't be the whole story, because EDD reported no errors but the protected backup failed. EDD should have no problem reproducing a track-claiming-to-be-another-track. So something else is going on. I've seen similar weirdness on disks from Electronic Arts (see 4am crack no. 1033 "Pinball Construction Set"). They used an extra wide track that spanned track 5 through 6. (Yes, you read that correctly.) That makes me wonder if this disk is also using an extra wide track, and I think I know how I can find out: by looking at track 5.5. --v-- TRACK: 05.50 START: 220D LENGTH: 17B7 21E8: FF FF FF FF FF FF FF FF VIEW 21F0: FF FF FF FF FF FF FF FF 21F8: FF FF FF FF FF FF FF FF 2200: FF FF FF FF FF FF FF FF 2208: FF FF FF FF FF D5 AA 96 <-220D ^^^^^^^^ prologue 2210: FF FE AA AF AA AA FF FB ^^^^^ ^^^^^ ^^^^^ T=$05 S=$00 chksm 2218: DE AA EB FF FF FF FF FF 2220: FF D5 AA AD 96 96 96 96 2228: 96 96 96 96 96 96 96 96 --^-- Track 5.5 is exactly the same as track 6: 16 complete sectors, all claiming to be track 5. Why didn't COPYA work? Track 6 is lying to us. Why didn't Locksmith FDB work? Likewise Why didn't my EDD copy work? Runtime protection check is somehow checking the extra wide track that spans tracks 5, 5.5, and 6 on side A. Next steps: 1. Find the protection check 2. Disable it 3. Go to the (home) gym ~ Chapter 1 To Everything (Turn, Turn, Turn) There Is A Season (Turn, Turn, Turn) I am working under the assumption that side B is unprotected. It can be copied by COPYA, and the copy boots and runs and eventually says PLEASE TURN DISK OVER AND PRESS SPACE BAR. So where is that, exactly. Turning to my trusty sector editor, I search for "BD 89 C0" (LDA $C089,X, one common way to turn on the drive motor). Side A has nothing at all; side B has several matches but nothing that looks suspicious. Hmm. I really don't want to trace this entire program from the boot sector. I search for "AD 00 C0" (LDA $C000, checks for a keypress), and I find a plethora of matches on side B: --v-- [$AD $00 $C0] ------------- DISK SEARCH ------------- $01/$0E-$3A $03/$01-$5C $09/$0C-$89 $09/$0C-$BC $09/$0C-$F2 $09/$0D-$6C $09/$0F-$79 $0A/$03-$3C $0A/$03-$72 $0A/$03-$EC $18/$06-$F1 $1B/$00-$04 $1D/$0F-$27 $21/$07-$DF $22/$08-$44 --^-- Hmm. Let's search for part of the message instead. Searching for "TURN" in high bit ASCII finds nothing, but re-searching in low bit ASCII finds two matches: --v-- ["TURN"] ------------- DISK SEARCH ------------- $09/$0E-$48 $0A/$00-$94 --^-- Promising! The match on track $09 looks like this: --v-- -------------- DISK EDIT -------------- TRACK $09/SECTOR $0E/VOLUME $FE/BYTE$48 --------------------------------------- $00: 0A 8D E1 B7 A9 22 8D EC ..a7)".l $08: B7 A9 0F 8D ED B7 A9 69 7)..m7)i $10: 8D F1 B7 4C 70 B7 8D 1A .q7Lp7.. $18: 62 A9 80 20 A8 FC AD 30 b). (|-0 $20: C0 88 D0 F5 60 29 62 34 @.Pu`)b4 $28: 62 0F 60 CA CF D9 D3 D4 b.`JOYST $30: C9 C3 CB 00 0F 60 CB C5 ICK..`KE $38: D9 C2 CF C1 D2 C4 00 09 YBOARD.. $40: 88 50 4C 45 41 53 45 20 .PLEASE $48:>54<55 52 4E 20 44 49 53 TURN DIS $50: 4B 20 4F 56 45 52 00 09 K OVER.. $58: 90 41 4E 44 20 50 52 45 .AND PRE $60: 53 53 20 53 50 41 43 45 SS SPACE $68: 20 42 41 52 2E 00 00 50 BAR...P $70: 44 4F 20 59 4F 55 20 57 DO YOU W $78: 41 4E 54 20 54 4F 20 44 ANT TO D --^-- That is definitely the message I see before flipping to side A. And there were several matches for the LDA $C000 instruction nearby. The one on track 9 looks like this: --v-- [T09,S0D] ----------- DISASSEMBLY MODE ---------- ; check for key 006C:AD 00 C0 LDA $C000 006F:10 FB BPL $006C ; is it SPACE 0071:C9 A0 CMP #$A0 ; no, loop back 0073:D0 F7 BNE $006C ; clear keyboard stroke 0075:8D 10 C0 STA $C010 ; turn on drive motor (I found this ; in my earlier search, but as you'll ; see, it's not suspicious) 0078:AE E9 B7 LDX $B7E9 007B:BD 89 C0 LDA $C089,X ; wait 007E:A9 00 LDA #$00 0080:20 A8 FC JSR $FCA8 ; check if disk is write-protected 0083:BD 8D C0 LDA $C08D,X 0086:BD 8E C0 LDA $C08E,X ; branch if it is not write-protected 0089:10 41 BPL $00CC ; otherwise immediately turn off the ; disk motor and continue 008B:BD 88 C0 LDA $C088,X --^-- This is almost certainly the routine that's waiting for me to insert side A. First, it's actually waiting for me to press SPACE, no other keys accepted. Second, it's checking whether the disk is write-protected. On the original disk, side B is write- protected but side A is not. If I don't actually flip the disk before pressing SPACE, the game briefly checks the disk then refuses to continue. This is how it knows: by checking whether the disk in the drive is write-protected. It's a quick check that doesn't even require reading the disk; the sensor is built in to the floppy drive. Just to be sure, I edited the sector (ON A BACKUP DISK OBVIOUSLY) and put a "JMP $FF59" at offset $78, and it immediately jumped to the monitor after I pressed SPACE. [S6,D1=hacked side B with JMP $FF59] [S5,D1=my work disk] ]PR#6 ...loads title screen... ...PLEASE TURN DISK OVER... ...beep... *C500G ...reboots slot 5... ]4C 59 FF 6178 Wait, what? Here's what: my work disk runs 4LIVE which is a delightful tool written by qkumba that allows me to take notes and annotate them while I'm cracking. It also adds a full memory search to the command prompt: just type some hex bytes and press , and it will print the addresses that match. Very useful in exactly this situation, where I've made changes on disk but don't know where my code ends up in memory. Let's go find that protection check. ~ Chapter 2 In Which We Zero In On It ]CALL -151 *6178L ; I put this here -- the original game ; had "LDX $B7E9" 6178- 4C 59 FF JMP $FF59 ; write-protect check 617B- BD 89 C0 LDA $C089,X 617E- A9 00 LDA #$00 6180- 20 A8 FC JSR $FCA8 6183- BD 8D C0 LDA $C08D,X 6186- BD 8E C0 LDA $C08E,X ; not write-protected -> branch 6189- 10 41 BPL $61CC The code from $618E to $61CB clears the screen, re-prints the same PLEASE TURN DISK OVER message, plays a few sounds, and jumps back to wait for a key. We'll take the branch and continue at $61CC. ; save some bytes to the stack 61CC- A2 13 LDX #$13 61CE- BD EC BC LDA $BCEC,X 61D1- 48 PHA 61D2- CA DEX 61D3- 10 F9 BPL $61CE ; copy $8800+ to $B800+ 61D5- A9 88 LDA #$88 61D7- 85 85 STA $85 61D9- A9 B8 LDA #$B8 61DB- 85 83 STA $83 61DD- A0 00 LDY #$00 61DF- 84 82 STY $82 61E1- 84 84 STY $84 61E3- B1 84 LDA ($84),Y 61E5- 91 82 STA ($82),Y 61E7- C8 INY 61E8- D0 F9 BNE $61E3 61EA- E6 85 INC $85 61EC- E6 83 INC $83 61EE- A5 83 LDA $83 ; until the target address hits $C000 61F0- C9 C0 CMP #$C0 61F2- D0 EF BNE $61E3 ; restore bytes from the stack 61F4- A2 00 LDX #$00 61F6- 68 PLA 61F7- 9D EC BC STA $BCEC,X 61FA- E8 INX 61FB- E0 14 CPX #$14 61FD- 90 F7 BCC $61F6 DOS 3.3 uses $B800-$BFFF for its RWTS, the low-level disk reading and writing routines. Lots of games that don't use DOS files nevertheless use its RWTS to read the disk. This is one of those. Looking at $8800 in memory, it looks like a standard RWTS -- $8D00 has the entry point code I would expect at $BD00, $8944 has the address field code I would expect at $B944, &c. ($8D4F has another "LDA $C089,X" that I found in my earlier search. Again, it's not suspicious because it's a legitimate part of the RWTS.) So we're copying an RWTS into place to use for reading side A, and the next few lines appear to set up a multi- sector read. ; $0A sectors 61FF- A9 0A LDA #$0A 6201- 8D E1 B7 STA $B7E1 ; first sector to read is T22,S0F ; (usually decremented) 6204- A9 22 LDA #$22 6206- 8D EC B7 STA $B7EC 6209- A9 0F LDA #$0F 620B- 8D ED B7 STA $B7ED ; first sector is stored at $6900 ; (usually decremented) 620E- A9 69 LDA #$69 6210- 8D F1 B7 STA $B7F1 6213- 4C 70 B7 JMP $B770 *B770L ; read the sectors B770- 20 93 B7 JSR $B793 ; jump to the code we just read B773- 4C 00 60 JMP $6000 We could... actually just run that, minus the final jump to $6000. But first, let's save the code we've captured so far. ; not sure how much to save here, but ; disk space is cheap, right? *BSAVE OBJ.6000-B7FF,A$6000,L$5800 ; now disconnect my work disk's DOS *FE89G FE93G ; RTS instead of JMP $6000 *B773:60 [S6,D1=side A (backup)] ; let the disk read itself *611CG ...read read read... ; reboot to my work disk *C500G ]BSAVE OBJ.6000-69FF,A$6000,L$A00 ]CALL -151 *6000L ; read one more sector from T04,S0F ; into $6A00 6000- A9 04 LDA #$04 6002- 8D EC B7 STA $B7EC 6005- A9 0F LDA #$0F 6007- 8D ED B7 STA $B7ED 600A- A9 01 LDA #$01 600C- 8D E1 B7 STA $B7E1 600F- A9 6A LDA #$6A 6011- 8D F1 B7 STA $B7F1 6014- 20 93 B7 JSR $B793 ... ; clear the hi-res screen (not shown) 605F- 20 A7 68 JSR $68A7 ... ; show the hi-res screen 60D6- AD 50 C0 LDA $C050 60D9- AD 54 C0 LDA $C054 60DC- AD 57 C0 LDA $C057 60DF- AD 52 C0 LDA $C052 60E2- 8D 10 C0 STA $C010 ... ; show hi-res page 1 and exit via a ; routine that loads the next phase ; from disk 6118- AD 54 C0 LDA $C054 611B- 4C 68 B7 JMP $B768 I'm pretty sure this is the series of screens where it asks you to type in your own message or press Esc to have the computer choose one for you. The code at $B768 is loading more from disk (at $6000, thus clobbering this code) then jumping to it. To wit: *B768L B768- A2 03 LDX #$03 B76A- 20 C2 B7 JSR $B7C2 B76D- 4C 00 60 JMP $6000 *B7C2L ; X is an index into arrays that set up ; another multi-sector read B7C2- BD 83 B7 LDA $B783,X B7C5- 8D E1 B7 STA $B7E1 B7C8- BD 87 B7 LDA $B787,X B7CB- 8D EC B7 STA $B7EC B7CE- BD 8B B7 LDA $B78B,X B7D1- 8D ED B7 STA $B7ED B7D4- BD 8F B7 LDA $B78F,X B7D7- 8D F1 B7 STA $B7F1 B7DA- 20 93 B7 JSR $B793 B7DD- 60 RTS I'm going to edit my backup copy of side A to break to the monitor at $611B instead of jumping to $B768. T22,S07,$1C: 68B7 -> 59FF Rebooting side B and flipping to side A when prompted, I get... nothing. It never makes it as far as $611B. Which is great, because it means I'm zeroing in on the protection check. ~ Chapter 3 In Which We Definitely Find It But Are Left With Questions About Its Seemingly Miraculous Appearance I spent quite a bit of time bisecting the code between $6000 and $611B to identify the call to the protection check. There's a loop at $6062 that paints twelve little sprites on the screen like a clock face. After it shows the hi-res screen (at $60D6), we have this series of calls: 60E5- 20 22 61 JSR $6122 60E8- 20 D2 61 JSR $61D2 60EB- 20 BA 62 JSR $62BA 60EE- 20 1C 63 JSR $631C 60F1- 20 9E 63 JSR $639E 60F4- 20 A5 64 JSR $64A5 60F7- 20 93 62 JSR $6293 60FA- 20 9E 63 JSR $639E 60FD- AD 21 61 LDA $6121 6100- F0 1C BEQ $611E 6102- 20 FB 63 JSR $63FB ... 611E- 4C E8 60 JMP $60E8 Almost all of these are conditional print routines. For example, $6122 might print the "type in your own message" prompt, if it's time to do that. $61D2 might print the "press Return when finished" message that displays after you type one character. $62BA might print the "enter your level of difficulty" message that displays if you press Esc (to have the game choose a random message instead of entering your own). &c. The whole thing loops back to $60E8 until $6121 is non-zero (checked at $60FD), at which point it falls through and calls $63FB, which unconditionally prints "prepare to enter the digital dimension." My non-working copy got that far, so I initially assumed that the protection check was called from within that subroutine. However, the subroutine is mercifully short and straightforward: *63FBL ; print "prepare to enter" 63FB- A2 0A LDX #$0A 63FD- A0 64 LDY #$64 63FF- 20 6B 68 JSR $686B ; print "the digital dimension" 6402- A2 1D LDX #$1D 6404- A0 64 LDY #$64 6406- 20 6B 68 JSR $686B ; return to caller 6409- 60 RTS First I thought, maybe the print routine at $686B has a sneaky condition in it that triggers the protection? I've seen that before on other disks. But no, it's just the table lookups and blitting you would expect from a hi-res character generator. Then I thought, maybe they're being REALLY sneaky and modifying the RTS at $6409, so by the time this subroutine is called it actually falls through to a protection check. To test that theory, I hacked my non- working copy of side A to jump to the monitor at $6102, instead of the JSR. T22,S07,$02: 20FB63 -> 4C59FF This causes the game to break to the monitor as expected. But to my chagrin, the subroutine at $63FB hadn't changed. But! Let's take another look at the caller: *6102L ; originally "JSR $63FB" ; (my copy reached here) 6102- 4C 59 FF JMP $FF59 6105- 8D 10 C0 STA $C010 6108- AD C6 68 LDA $68C6 610B- D0 FB BNE $6108 610D- A2 05 LDX #$05 610F- B5 FA LDA $FA,X 6111- 29 7F AND #$7F 6113- 95 FA STA $FA,X 6115- CA DEX 6116- 10 F7 BPL $610F 6118- 20 04 70 JSR $7004 ; originally "JMP $B768" ; (my copy did NOT reach here) 611B- 4C 59 FF JMP $FF59 The code at $6102 and $611B are the two instructions I replaced with jumps to the monitor. But wait, what's this at $6118? 6118- 20 04 70 JSR $7004 That... was not there before. I mean, I literally saved this chunk of memory to my work disk earlier, and it was this: 6118- AD 54 C0 LDA $C054 611B- 4C 68 B7 JMP $B768 And now it is... not that. I am suddenly VERY interested to learn what ends up at $7004. *7004L ; track 4 7004- A9 04 LDA #$04 7006- 8D EC B7 STA $B7EC 7009- AE F4 B7 LDX $B7F4 700C- 8E 03 70 STX $7003 ; seek command 700F- A2 00 LDX #$00 7011- 8E F4 B7 STX $B7F4 7014- 8E 01 70 STX $7001 ; Death Counter? 7017- A0 04 LDY #$04 7019- 8C 00 70 STY $7000 ; seek to track 4 701C- A9 B7 LDA #$B7 701E- A0 E8 LDY #$E8 7020- 20 B5 B7 JSR $B7B5 ; turn on drive motor manually ; (highly suspicious -- also, notice ; that it uses STA $C089,X instead of ; LDA $C089,X, which works just as ; well but evaded my earlier search for ; the more common variant) 7023- AE F7 B7 LDX $B7F7 7026- 9D 89 C0 STA $C089,X ; initially puzzling, but it appears ; that this RWTS keeps track of the ; current track number in $BCF0 instead ; of the usual screen hole in the text ; page 7029- A0 08 LDY #$08 702B- 8C F0 BC STY $BCF0 702E- C8 INY 702F- C8 INY 7030- 98 TYA 7031- 48 PHA ; Standard DOS seek routine takes a ; half-track (a.k.a. "phase") in A. ; Y was 8, incremented twice = 10, ; transferred to A, so we're seeking to ; track 10/2 = 5 7032- 20 A0 B9 JSR $B9A0 ; read the next available address field 7035- 20 44 B9 JSR $B944 7038- B0 0B BCS $7045 ; read the data field 703A- 20 DC B8 JSR $B8DC 703D- B0 06 BCS $7045 ; check the track number (saved in zero ; page by the address field parser at ; $B944, which we just called) 703F- A9 05 LDA #$05 7041- C5 2E CMP $2E ; track = 5 -> good, branch ahead 7043- F0 18 BEQ $705D ; track != 5 -> bad, push a bogus ; address to the stack and crash ; (we may also end up here from $7038 ; or $703D, if we didn't successfully ; find and parse an address field or ; a data field) 7045- A9 C6 LDA #$C6 7047- 48 PHA 7048- A9 03 LDA #$03 704A- 48 PHA 704B- A9 FF LDA #$FF 704D- 8D 01 70 STA $7001 7050- 60 RTS ... ; pull phase that we pushed earlier ; (currently #$0A) 705D- 68 PLA 705E- A8 TAY 705F- C0 0C CPY #$0C 7061- D0 CC BNE $702F We're branching back a few times, but if you look closely, you'll see that we're branching back to the second INY instruction, not the first. That means A will be #$0B the second time we call $B9A0. So we're doing everything again, but on track 5.5 instead. Then we'll branch back a third time, but Y will be incremented once more and A will be #$0C going into $B9A0. So we'lll do everything once again, but on track 6 instead. This is the heart of the protection: an extra wide track that spans tracks 5, 5.5, and 6. As we saw earlier in the nibble editor, all the sectors on track 5.5 and track 6 claim to be track 5. That would be impossible to replicate with a standard bit copier. Funnily enough, this protection check always passes on the first iteration, because it's on track 5 and reading sectors and checking that they claim to be on track 5. Which, even on my copy, is true. But then the loop continues with tracks 5.5 and track 6, and it all falls apart because my track 6 sectors claim to be track 6 because they are not psychopaths. Continuing from $7063... ; decrement counter (initially 4, set ; at $7019) 7063- CE 00 70 DEC $7000 ; seek to track 4 again 7066- A9 08 LDA #$08 7068- 20 A0 B9 JSR $B9A0 ; do it all again, repeatedly 706B- AC 00 70 LDY $7000 706E- D0 A9 BNE $7019 So this check has to pass 4 times in a row, or it's off to The Badlands. ; store flag (maybe checked later?) 7070- 8C 02 70 STY $7002 ; turn off drive motor 7073- 9D 88 C0 STA $C088,X ; restore RWTS parameter table 7076- AD 03 70 LDA $7003 7079- 8D F4 B7 STA $B7F4 ; one final check (The Badlands sets ; this address to #$FF, so I guess even ; if you manage to escape from there, ; we want to make extra sure you never ; return from this protection check) 707C- AD 01 70 LDA $7001 707F- F0 03 BEQ $7084 7081- 4C 00 C6 JMP $C600 ; and finally return to the caller 7084- 60 RTS Whew. Now, where in the world did this copy protection routine... come from? ~ Chapter 4 In Which The Code Is Coming From Inside The House First of all, it was not loaded from disk. I captured everything that was loaded from disk (OBJ.6000-B7FF), and $7000 was something else altogether: *BLOAD OBJ.6000-B7FF *7000L 7000- A9 00 LDA #$00 7002- 8D F0 03 STA $03F0 7005- A9 C6 LDA #$C6 7007- 8D F1 03 STA $03F1 700A- A9 0C LDA #$0C 700C- 8D E1 B7 STA $B7E1 700F- A9 0A LDA #$0A 7011- 8D EC B7 STA $B7EC 7014- A9 0F LDA #$0F 7016- 8D ED B7 STA $B7ED 7019- A9 A0 LDA #$A0 701B- 8D F1 B7 STA $B7F1 701E- 20 93 B7 JSR $B793 That's just part of the previous phase of the boot. It bears no resemblance to the copy protection routine that ended up at $7004. The short answer is, everywhere. The more I look, the more I find. There is no part of this code that is not involved, in some small way, in constructing this protection routine out of thin air. Remember the code at $6000, which read a sector from track $04? Here's the "harmless" code immediately after that: *BLOAD OBJ.6000-69FF *6017L 6017- A9 00 LDA #$00 6019- 85 F8 STA $F8 ; store #$00 in two addresses ; within the protection routine 601B- 8D 1A 70 STA $701A <-- ! 601E- 8D 6C 70 STA $706C <-- ! 6021- A2 05 LDX #$05 ; store #$05 in one address ; within the protection routine 6023- 8E 40 70 STX $7040 <-- ! ; store #$A0 in several zero page ; addresses, and also several addresses ; within the protection routine ; (this is the LDY opcode) 6026- A9 A0 LDA #$A0 6028- 95 FA STA $FA,X 602A- CA DEX 602B- 10 FB BPL $6028 602D- 8D 17 70 STA $7017 <-- ! 6030- 8D 33 70 STA $7033 <-- ! 6033- 8D 69 70 STA $7069 <-- ! 6036- 8D 1E 70 STA $701E <-- ! 6039- 8D 29 70 STA $7029 <-- ! 603C- A9 02 LDA #$02 603E- 85 24 STA $24 6040- 4A LSR ; store #$01 in one address ; within the protection routine 6041- 8D 4E 70 STA $704E <-- ! ; store #$00 in several zero page ; addresses, and also one address ; within the protection routine 6044- A9 00 LDA #$00 6046- 85 11 STA $11 6048- 85 22 STA $22 604A- 85 23 STA $23 604C- 8D 54 70 STA $7054 <-- ! ; store #$20 in several addresses ; within the protection routine ; (this is the JSR opcode) 604F- A9 20 LDA #$20 6051- 8D 32 70 STA $7032 <-- ! 6054- 8D 3A 70 STA $703A <-- ! 6057- 85 25 STA $25 6059- 8D 68 70 STA $7068 <-- ! I thought nothing of these at the time (who cares about initializing addresses to zero? or any other constant?) but know we know better. Now we know that an important routine ends up in memory at $7004..$7084. And this is how it gets there: literally one byte at a time. Here is the routine that conditionally prints the "type in your own message" prompt: *6122L 6122- AD 35 64 LDA $6435 6125- D0 01 BNE $6128 6127- 60 RTS 6128- C9 02 CMP #$02 612A- D0 78 BNE $61A4 612C- AA TAX 612D- E8 INX ; store #$03 in several places ; within the protection routine 612E- 8E 80 70 STX $7080 <-- ! 6131- 8E 0D 70 STX $700D <-- ! 6134- 8E 77 70 STX $7077 <-- ! 6137- 8E 49 70 STX $7049 <-- ! 613A- E8 INX ; store #$04 in several places ; within the protection routine 613B- 8E 18 70 STX $7018 <-- ! 613E- 8E 05 70 STX $7005 <-- ! ; actually print the prompt 6141- A2 CF LDX #$CF 6143- A0 64 LDY #$64 6145- 20 6B 68 JSR $686B 6148- A2 EE LDX #$EE 614A- A0 64 LDY #$64 614C- 20 6B 68 JSR $686B Oh, remember I mentioned "a loop at $6062 that paints twelve little sprites on the screen like a clock face" COMPLETELY CASUALLY, IN PASSING, LIKE IT WAS NOTHING. The sprite painting routine starts at $6708, but it doesn't end until it passes through this code at $67ED: *67EDL 67ED- A9 E5 LDA #$E5 67EF- 8D 1F 61 STA $611F 67F2- A9 04 LDA #$04 67F4- 8D 19 61 STA $6119 <-- ! 67F7- A9 60 LDA #$60 67F9- 8D 20 61 STA $6120 67FC- A9 70 LDA #$70 67FE- 8D 1A 61 STA $611A <-- ! 6801- AD E5 60 LDA $60E5 6804- 8D 18 61 STA $6118 <-- ! $60E5 is #$20, so we've put "20 04 70" at $6118, a.k.a. "JSR $7004", the call to the protection routine. But wait, there's more. 6807- AD EB 62 LDA $62EB ; =$70 680A- 85 03 STA $03 680C- A9 00 LDA #$00 680E- 85 02 STA $02 ; copy $52 bytes into various addresses ; in the $7000 page, using an index ; table to find the offset of each byte 6810- A2 52 LDX #$52 6812- BD 62 66 LDA $6662,X 6815- BC B5 66 LDY $66B5,X 6818- 91 02 STA ($02),Y 681A- CA DEX 681B- 10 F5 BPL $6812 681D- 60 RTS And that's how you build a protection routine out of thin air. ~ Chapter 5 In Which We Are Not Amused Now that we've... uncovered? Revealed? what's the word to describe seeing what was right in front of your face the whole time? Anyway, now that we've done that, we should be able to bypass the protection check by changing the instruction that sets up the call to $7004. $6801 stores #$20 at $6118 (the JSR opcode), but if we put #$AD there instead, "JSR $7004" turns into a harmless "LDA $7004" and execution will continue. [S6,D1=fresh copy of side A] ; LDA $60E5 -> LDA #$AD / NOP T22,S0E,$01: ADE560 -> A9ADEA Rebooting side B, flipping to side A when prompted, "preparing to enter the digital dimension," and... Success! The first level's background graphic loads, and the game begins. Except... Willy Byte is supposed to make his grand entrance by falling from a chute in the ceiling; his parachute opens, and he lands safely on the playing field near the bottom of the screen. But his parachute never opens, and he falls through the bottom of the screen and reappears at the top, endlessly. Ha ha! Amusing in a quiet way, said Eeyore, but not really helpful. ~ Chapter 6 Two Holes Are Better Than One; Any Mouse Will Tell You That After (not) calling the protection check at $7004, execution continues at $B768, which looks like this: B768- A2 03 LDX #$03 B76A- 20 C2 B7 JSR $B7C2 B76D- 4C 00 60 JMP $6000 I can hack my non-working copy to jump to the monitor at $B76D instead of continuing to $6000. This code is on side B, all the way back on track 0. [S6,D1=copy of side B] T00,S01,$6E: 0060 -> 59FF Rebooting side B, switching to side A, and attempting to start level 1, and... * Success! I've broken into the monitor with the level 1 game code in memory. ...Where, exactly? Going back to $B768, it calls $B7C2 with X=3. Here's $B7C2 (still in memory): *B7C2L B7C2- BD 83 B7 LDA $B783,X B7C5- 8D E1 B7 STA $B7E1 B7C8- BD 87 B7 LDA $B787,X B7CB- 8D EC B7 STA $B7EC B7CE- BD 8B B7 LDA $B78B,X B7D1- 8D ED B7 STA $B7ED B7D4- BD 8F B7 LDA $B78F,X B7D7- 8D F1 B7 STA $B7F1 B7DA- 20 93 B7 JSR $B793 B7DD- 60 RTS We're setting up RWTS parameters for a multi-sector read. Checking the third byte of each array, the final parameter table looks like this: $B786 -> $B7E1 = $28 (sector count) $B78A -> $B7EC = $14 (start track) $B78E -> $B7ED = $02 (start sector) $B792 -> $B7F1 = $87 (start address) Which means we read $28 sectors into $6000..$87FF. ; reboot to my work disk *C500G ; save newly loaded game code ]BSAVE OBJ.6000-87FF,A$6000,L$2800 Of note: this completely clobbers the previous protection check (at $7004+), that check's success flag (at $7002), and its caller (at $6000+). The game is not failing because it noticed that we bypassed the previous protection check; it's failing for some other reason. Like a second protection check. Unfortunately (for me), there is no hard failure in this second protection check. I can't bisect the code to find it, because all it does is set some global flag that the game checks later. But sometimes you get lucky. On the theory that there is only one structural protection on this disk (the extra wide track that spans tracks 5, 5.5, and 6), the second protection check probably looks similar to the first. Of course, the first check was constructed byte by byte in memory, but maybe... ]CALL -151 *20 A0 B9 8173 8194 Aha! 4LIVE's memory search function to the rescue! (Thanks, qkumba.) These are both part of the same routine starting at $8148. ($8147 is an RTS.) *8148L ; seek to track 4 8148- A9 04 LDA #$04 814A- 8D EC B7 STA $B7EC 814D- AD F4 B7 LDA $B7F4 8150- 8D A7 81 STA $81A7 8153- A2 00 LDX #$00 8155- 8E F4 B7 STX $B7F4 8158- A0 01 LDY #$01 815A- 8C A6 81 STY $81A6 815D- A9 B7 LDA #$B7 815F- A0 E8 LDY #$E8 8161- 20 B5 B7 JSR $B7B5 ; turn on drive motor 8164- AE F7 B7 LDX $B7F7 8167- 9D 89 C0 STA $C089,X 816A- A0 08 LDY #$08 816C- 8C F0 BC STY $BCF0 816F- C8 INY 8170- C8 INY 8171- 98 TYA 8172- 48 PHA ; seek to track 5 (later 5.5 and 6) 8173- 20 A0 B9 JSR $B9A0 ; read and parse address field 8176- 20 44 B9 JSR $B944 8179- B0 0B BCS $8186 ; read and verify data field 817B- 20 DC B8 JSR $B8DC 817E- B0 06 BCS $8186 ; check if track number = 5 8180- A9 05 LDA #$05 8182- C5 2E CMP $2E ; yes -> branch over next instruction 8184- F0 03 BEQ $8189 ; no -> set global flag (aha!) 8186- EE 6E 69 INC $696E ; continue for tracks 5.5 and 6 8189- 68 PLA 818A- A8 TAY 818B- C0 0C CPY #$0C 818D- D0 E1 BNE $8170 ; decrement counter, but it was ; initialized to 1 (at $815A) so we ; only bother doing this protection ; check once 818F- CE A6 81 DEC $81A6 ; reset disk to track 4 8192- A9 08 LDA #$08 8194- 20 A0 B9 JSR $B9A0 ; this actually never branches 8197- AC A6 81 LDY $81A6 819A- D0 BE BNE $815A ; turn off drive motor 819C- 9D 88 C0 STA $C088,X ; restore RWTS parameters we changed 819F- AD A7 81 LDA $81A7 81A2- 8D F4 B7 STA $B7F4 ; exit to caller regardless of success ; or failure 81A5- 60 RTS I was right: there is a second check, and it does set a global flag ($696E) on failure. Searching both sides of the disk for "F0 03 EE 6E 69" (from $8184), I find this protection check on track $13. I should be able to put an RTS at the beginning of the routine to bypass it. [S6,D1=non-working copy of side A] T13,S0C,$48: A9 -> 60 Rebooting side B, switching to side A, and attempting to start level 1, and... Willy Byte never appears at all, and there are graphical glitches flashing along the side of the screen. Oh bother, said Pooh. Oh help and bother. ~ Chapter 7 Mammas Don't Let Your Babies Grow Up To Build Interpreters Disabling the first protection check at $7004 (by not calling it) worked. I'm confident that there are no lingering side effects that could be detected. Disabling the second protection check at $8148 (by putting an RTS at $8148) did not work. This check has no side effects unless it fails, so either there is a THIRD protection check, or there is some sort of tamper check elsewhere in the code that is detecting that I disabled the protection check at $8148. Or both. ; reboot to my work disk ]PR#5 ; reload level 1 game code ]BLOAD OBJ.6000-87FF ]CALL -151 ; search for references to $8148 *48 81 74D2 Aha! There is one reference to $8148 elsewhere in memory. *74D1L 74D1- AD 48 81 LDA $8148 74D4- C9 A9 CMP #$A9 74D6- D0 78 BNE $7550 Damn it. They anticipated my approach. 74D8- CD 5D 81 CMP $815D 74DB- D0 71 BNE $754E Double damn it. 74DD- CD 80 81 CMP $8180 74E0- D0 6A BNE $754C Triple. 74E2- CD 92 81 CMP $8192 74E5- D0 63 BNE $754A Quadruple. That's all the comparisons I found; the rest of the routine does something unrelated. Which is good, because I'm running out of curse words, and I don't know what comes after "quadruple." I bypassed the first protection check by changing the calling code, not the protection code. Maybe I should do that here. Then the tamper check would pass, since the copy protection code would not, in fact, have been tampered with. It just wouldn't get called, which is fine because it has no side effects. Unfortunately, I already searched for references to $8148 in memory, and all I found was the tamper check at $74D1. So how does this protection check ever get called? Theory: the game may be pushing the address to the stack directly in order to "return" to it later. I've seen this sort of obfuscation on other disks. ; search memory for "LDA #$81" *A9 81 69CE 7857 Let's look at $69CE first. *69CEL 69CE- A9 81 LDA #$81 69D0- 8D AC 6E STA $6EAC That seems harmless enough on its own, until you look at $6EAC and see that it's executable code. 6EA6- C6 92 DEC $92 6EA8- F0 05 BEQ $6EAF 6EAA- A9 01 LDA #$01 6EAC- 8D E7 84 STA $84E7 <-- ? 6EAF- 60 RTS #$81 is a valid 6502 opcode, but it's not a very widely used one. (It stores the accumulator in an indirect address pointed to by a zero page address which is itself indexed by X, which is mostly useful if you're writing an interpreter or you're trying to obfuscate something else. Dear God, please don't let it be an interpreter.) Unless... Maybe we're not looking for stack manipulation at all. Maybe we've (once again) stumbled on part of a multi- faceted campaign to construct the code that calls a protection check. ; look for references to $6EAB *AB 6E 8604 Looking at surrounding code, it appears that this is part of a subroutine that starts at $85F2. ($85F1 is an RTS.) *85F2L 85F2- A9 20 LDA #$20 85F4- 85 03 STA $03 85F6- A9 40 LDA #$40 85F8- 85 01 STA $01 85FA- A9 00 LDA #$00 85FC- 85 02 STA $02 85FE- 85 00 STA $00 8600- A8 TAY 8601- A9 48 LDA #$48 8603- 8D AB 6E STA $6EAB <-- ! 8606- B1 02 LDA ($02),Y 8608- 91 00 STA ($00),Y 860A- C8 INY 860B- D0 F9 BNE $8606 860D- E6 01 INC $01 860F- A5 03 LDA $03 8611- 18 CLC 8612- 69 01 ADC #$01 8614- 85 03 STA $03 8616- C9 40 CMP #$40 8618- D0 EC BNE $8606 861A- 60 RTS This routine copies hi-res page 2 ($4000) to hi-res page 1 ($2000). Which is fine, except in the middle it sets $6EAB to #$48 for absolutely no reason. I bet... ; search memory for references to $6EAA *AA 6E 76C5 *76C1L 76C1- A9 26 LDA #$26 76C3- 0A ASL 76C4- 8D AA 6E STA $6EAA Oh that's clever. #$26 shifted left is #$4C, the JMP opcode. (This is, again, stuck in the middle of unrelated code.) Assuming that all of these routines are executed at some point, $6EAA ends up being "4C 48 81", a.k.a. "JMP $8148", a.k.a. "jump to the second protection check." The RTS opcode is #$60, shifting right gives me #$30. I should be able to bypass the jump to the protection check by changing $76C2 to #$30. ; undo my RTS patch at $8148 T13,S0C,$48: 60 -> A9 ; change code that constructs the JMP ; opcode at $6EAA so it constructs an ; RTS instead T13,S01,$C2: 26 -> 30 Now the protection check itself is unmodified, which means the tamper check at $74D1 shouldn't complain. And the code that constructs a JMP to the protection check at $8148 now puts an RTS there instead. Rebooting side B, switching to side A, and attempting to start level 1, and... The game reboots. I am not enjoying this nearly as much as you might think. ~ Chapter 8 In Which We Give It Everything We've Got Disabling the second protection check at $8148 (by modifying the code that builds the caller at runtime) did not work. This check has no side effects unless it fails, so either there is a THIRD protection check, or there is a SECOND tamper check detecting that I modified the code that builds the code that calls the protection check. At this point, neither option would surprise me. Nor both. ]PR#5 ]BLOAD OBJ.6000-87FF ]CALL -151 ; search memory for references to $76C2 ; (the byte I modified) *C2 76 63F9 *63F1L 63F1- AD C1 76 LDA $76C1 63F4- C9 A9 CMP #$A9 63F6- D0 F0 BNE $63E8 63F8- AD C2 76 LDA $76C2 63FB- C9 26 CMP #$26 63FD- D0 E9 BNE $63E8 63FF- AD C3 76 LDA $76C3 6402- C9 0A CMP #$0A 6404- D0 E2 BNE $63E8 6406- AD C4 76 LDA $76C4 6409- C9 8D CMP #$8D 640B- D0 DB BNE $63E8 640D- AD C5 76 LDA $76C5 6410- C9 AA CMP #$AA 6412- D0 D4 BNE $63E8 6414- AD C6 76 LDA $76C6 6417- C9 6E CMP #$6E 6419- D0 CD BNE $63E8 641B- 60 RTS ARE YOU KIDDING ME?!? They are explicitly checking that no one has tampered with the six bytes of code responsible for putting a single JMP opcode into place to call the protection check that I can't change because of a different tamper check. QUINTUPLE DAMN IT. (I looked it up.) This is as good a time as any to tell you that I also looked at $7857 the second match for "LDA #$81". (The first match was $69CE, which built the JMP $8148 at $6EAA.) I am happy -- nay, thrilled -- to report that $7857 is part of a THIRD tamper check. *7853L ; set up ($02) to point to $8148 ; (the second protection check) 7853- A9 48 LDA #$48 7855- 85 02 STA $02 7857- A9 81 LDA #$81 7859- 85 03 STA $03 ; check $31 different nonsequential ; bytes of the protection check against ; an array of expected values 785B- A2 31 LDX #$31 785D- BD 70 78 LDA $7870,X 7860- BC F9 69 LDY $69F9,X 7863- D1 02 CMP ($02),Y ; if any fails, branch to a BRK (which ; crashes and reboots) 7865- D0 05 BNE $786C 7867- CA DEX 7868- 10 F3 BPL $785D 786A- 60 RTS 786B- C9 00 CMP #$00 <-- BRK 786D- D0 EE BNE $785D 786F- 60 RTS The only reason this tamper check didn't trigger a crash when I put an RTS at $8148 is that that's not one of the $31 bytes it checks (presumably because the author knows that $8148 is checked elsewhere). It was just dumb luck that I tripped that tamper check instead of this one. Thus... We shall disable the protection code AND the tamper check on the protection code AND the code that builds the code that calls the protection code AND the tamper check on that code AND THEN THEY'LL ALL BE SORRY. (*) (*) not guaranteed, actual sorrow may vary Here we go. ; disable protection check at $8148 ; (again) T13,S0C,$48: A9 -> 60 ; change code at $76C1 that constructs ; the JMP at $6EAA so it constructs an ; RTS instead T13,S01,$C2: 26 -> 30 ; disable failure branches in $74D1 ; tamper check T12,S0F,$D7: 78 -> 00 T12,S0F,$DC: 71 -> 00 T12,S0F,$E1: 6A -> 00 T12,S0F,$E6: 63 -> 00 ; disable failure branch in $7853 ; tamper check T13,S03,$65: 05 -> 00 Rebooting side B, switching to side A, and attempting to start level 1, and... The game plays! ...until level 2, when it crashes. ~ Chapter 9 In Which We Start Over Again, I don't know what's going on. Either there is a FOURTH tamper check that has detected one of the multitude of changes I've made to disable the second protection check, or there is a third protection check. Or, you know, both. At this point, no reasonable person would bet against the "both." On the hunch that there is a third protection check, and that it is similar to the first two protection checks, I returned to my trusty sector editor and searched for "JSR $B9A0", the call to the track seek routine. Side B had no matches (is there anything from side B still in memory, besides the RWTS?) but side A is a different story. --v-- [$20 $A0 $B9] ------------- DISK SEARCH ------------- $13/$0C-$73 $13/$0C-$94 $17/$01-$1A $17/$01-$39 $1A/$08-$09 $1A/$08-$28 --^-- I know about the code on track $13; that's the second protection check at $8148. Here's the code on track $17 (actually starts on sector 0): [T17,S00] ----------- DISASSEMBLY MODE ---------- 00EF:A9 04 LDA #$04 00F1:8D EC B7 STA $B7EC 00F4:AD F4 B7 LDA $B7F4 00F7:8D 4C 81 STA $814C 00FA:A2 00 LDX #$00 00FC:8E F4 B7 STX $B7F4 00FF:A0 02 LDY #$02 ... [T17,S01] 0001:8C 4B 81 STY $814B 0004:A9 B7 LDA #$B7 0006:A0 E8 LDY #$E8 0008:20 B5 B7 JSR $B7B5 000B:AE F7 B7 LDX $B7F7 000E:9D 89 C0 STA $C089,X 0011:A0 08 LDY #$08 0013:8C F0 BC STY $BCF0 0016:C8 INY 0017:C8 INY 0018:98 TYA 0019:48 PHA 001A:20 A0 B9 JSR $B9A0 001D:20 44 B9 JSR $B944 0020:B0 0B BCS $002D 0022:20 DC B8 JSR $B8DC 0025:B0 06 BCS $002D 0027:A9 05 LDA #$05 0029:C5 2E CMP $2E 002B:F0 01 BEQ $002E 002D:00 BRK <-- ! 002E:68 PLA 002F:A8 TAY 0030:C0 0C CPY #$0C 0032:D0 E3 BNE $0017 0034:CE 4B 81 DEC $814B 0037:A9 08 LDA #$08 0039:20 A0 B9 JSR $B9A0 003C:AC 4B 81 LDY $814B 003F:D0 C0 BNE $0001 0041:9D 88 C0 STA $C088,X 0044:AD 4C 81 LDA $814C 0047:8D F4 B7 STA $B7F4 004A:60 RTS --^-- I could change the BRK at offset $2D to NOP and it would fall through, but I bet it's tamper checked somewhere. Or I could disable the caller; based on the absolute address it uses for temporary storage ($814B), this code is loaded at $80EF. Searching both sides for "EF 80" finds several matches: --v-- [$EF $80] ------------- DISK SEARCH ------------- $15/$00-$F3 $17/$00-$C9 $17/$0A-$6F --^-- So there are (at least) three callers to the third protection check. I did not look for tamper checks. The FOURTH protection check is on track $1A. I won't list it, but it starts at T1A,S07,$DE and continues to sector 8. It works exactly the same way as the first, second, and third checks. It is loaded at $73DE. It is called via JSR at T1A,S09,$1B. I did not look for tamper checks. Dear reader, I am tired of this game. Not the game itself, which is lovely. I'm tired of the meta-game, wherein a skilled developer -- decades hence -- put a whole bunch of thought into how their copy protection would run and how other people would try to break it, and came up with a masterful combination of - obfuscation: building code and callers at runtime - redundancy: 4+ protection checks, 7+ calls, many delayed - paranoia: 3+ tamper checks, come on I'm tired of this game because I'm losing. ~ Chapter 10 In Which We Change The Game There were other companies renowned for their copy protection. Electronic Arts famously used an extra wide track, much like this disk. They also invented an entire virtual machine and interpreter to execute their protection routines... and their tamper checks. (See 4am crack no. 1033 "Pinball Construction Set" for all that.) This disk does not go quite that far. Besides being written in 6502 assembly (whew), there is another significant difference. The protection checks in EA's games manipulated low-level drive softswitches directly to move the drive head and read the disk. But all of the protection checks on this disk use the standard DOS 3.3 RWTS routines to seek to a track, parse an address field, &c. The DOS 3.3 RWTS is, in essence, an abstraction. Call this routine and the drive head will move to a given track. Call this routine and it will find and parse the next available address field. Granted, that's still pretty low level, but it gives me an opening. What if the track seek routine... lied? Hear me out. The way each protection check is structured, it succeeds on the first iteration, even on a non-original disk. It seeks to track 5 and checks that a sector claims to be on track 5. It's only the second and third times through, after it seeks to track 5.5 or 6, that the check fails. What if the track seek routine at $B9A0 just... didn't always seek to the track you requested? Crucially, none of the four protection routines verify, after calling $B9A0, that the drive actually seeked to the track they asked for. If we can keep the drive on track 5, instead of moving to track 5.5 or 6, the protection check will loop three times and succeed every time and think it has an original disk WITHOUT ANY CHANGES TO THE PROTECTION CODE AT ALL. And we CAN keep the drive on track 5, because the vital function of "seeking to a track" has been outsourced to the RWTS routine at $B9A0. And I bet they're not tamper-checking the RWTS. Let's find out. The track seek routine for side A (remember, side A has its own RWTS for some reason -- it's copied from $8800+ after you flip the disk) starts like this: B9A0- 86 2B STX $2B B9A2- 85 2A STA $2A B9A4- CD F0 BC CMP $BCF0 B9A7- F0 53 BEQ $B9FC I'm going to patch the first four bytes so it calls a routine at $BA69 instead: B9A0- 4A LSR <-- B9A1- 20 69 BA JSR $BA69 <-- B9A4- CD F0 BC CMP $BCF0 B9A7- F0 53 BEQ $B9FC $BA69 is a small range of unused space within the RWTS. Later versions of DOS used it for patches, but this game only uses the RWTS, not the rest of DOS. Here's what I put at $BA69: ; Because of the LSR at $B9A0, the ; accumulator is now the actual (whole) ; track number, not a phase. ; We check if we've been asked to seek ; to a track that we know is only used ; by the protection checks. BA69- C9 06 CMP #$06 BA6B- D0 02 BNE $BA6F ; if so, move to track 5 instead BA6D- A9 05 LDA #$05 ; shift to turn the track into a phase ; again (the rest of the seek routine ; expects this) BA6F- 0A ASL ; replicate the original code at $B9A0 BA70- 86 2B STX $2B BA72- 85 2A STA $2A ; return to caller (execution will ; continue at $B9A4) BA74- 60 RTS In essence, this RWTS selectively lies. If you ask to seek to track 4, or 7, or 23, or almost anything, it will do it. But if you ask to seek to track 6, it will actually move to track 5. Additionally, if you ask to seek to ANY half track (like 5.5), it will actually move to the whole track before it (like 5). Then it will return gracefullly and claim it succeeded. Remember, none of the protection checks verify the track after seeking. The RWTS for side A is on track $10 of side B. Starting with a fresh copy of both sides of the disk, I made these changes to side B: T10,S01,$A0: 862B852A -> 4A2069BA T10,S02,$69 -> C906D002A9050A862B852A60 I made no changes whatsoever to side A. Rebooting side B, switching to side A, and attempting to start level 1, and... The game plays. On to level 2, and... The game plays. On to level 3, and... The game plays. &c. All copies of the protection code (at least 4) remain intact. All calls to the protection code (at least 7) remain intact. All the code that builds the code that calls the other code, remains intact. All the code that checks that the code that builds the code that calls the other code, remains intact. In fact, none of the tamper checks scattered throughout the game code ever trip. Why would they? None of the game code has been tampered with. All protection routines are executed as expected, and they all pass. They're just no longer checking what they think they're checking. They had one fatal flaw: they entrusted a vital function (track seek) to an untrusted RWTS. When that RWTS started lying, they continued to trust it. And all the checks upon checks upon checks were rendered moot. Quod erat liberandum. ~ Acknowledgements Many thanks to Murray Krehbiel -- the original game developer AND protection developer -- for sending me a boxed retail copy of his game to preserve and document. Also thanks to qkumba for playtesting several incomplete versions of this crack, plus the final working version. --------------------------------------- A 4am crack No. 2698 ------------------EOF------------------