----------The Spy Strikes Back--------- A 4am crack 2018-08-04 --------------------------------------- Name: The Spy Strikes Back! Genre: arcade Year: 1983 Credits: Robert Hardy, Mark Pelczarski Publisher: Penguin Software Platform: Apple ][+ or later Media: 5.25-inch disk Sides: 1 OS: custom Similar cracks: #1748 Bouncing Kamungas #1676 Thunder Bombs ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways COPYA immediate disk read error, but it gets a participation certificate that spells out "You Tried" Locksmith Fast Disk Backup unable to read any track EDD 4 bit copy to .nib file on CFFA3000 read errors on T12+ copy fails to boot, hangs with the drive motor off Copy ][+ nibble editor tracks $12-$22 appear unformatted Lower tracks have modified epilogues ("DA AA EB" instead of "DE AA EB") Odd tracks also have custom prologue ("D4 AA 96" instead of "D5 AA 96") no sign of anything beyond track $11 no half or quarter tracks Disk Fixer "O" for INPUT/OUTPUT CONTROL, set the epilogues to "DA AA EB" track 0 is readable boot sector is custom no sign of disk catalog on any track no sign of DOS or ProDOS or any OS whatsoever Why didn't COPYA work? modified prologues/epilogues on every track Why didn't Locksmith FDB work? ditto Why didn't my .nib image work? This is a bit of a mystery. The alternating D4/D5 prologues shouldn't pose any problem for a nibble copier, so I'm assuming there is some sort of nibble check early in the boot. Next steps: 1. Run Passport to convert the disk to a standard format 2. Patch the bootloader to read the newly standardized disk 3. Find and disable the nibble check 4. Declare victory (*) ~ Chapter 1 In Which Our Automated Tools Take Us Just About As Far As They Can Passport successfully auto-converted the disk to a standard format. The built-in RWTS was designed to read this kind of alternating D4/D5 prologue (it was very common), and the adaptive RWTS correctly determined the first epilogue nibble (DA) and enforced it on the rest of the sectors. More information and source code is available at https://archive.org/details/Passport4am Here is the Passport transcript: --v-- READING FROM S6,D1 USING BUILT-IN RWTS T22 IS UNFORMATTED WRITING TO RAM DISK T21 IS UNFORMATTED T20 IS UNFORMATTED T1F IS UNFORMATTED T1E IS UNFORMATTED T1D IS UNFORMATTED T1C IS UNFORMATTED T1B IS UNFORMATTED T1A IS UNFORMATTED T19 IS UNFORMATTED T18 IS UNFORMATTED T17 IS UNFORMATTED T16 IS UNFORMATTED T15 IS UNFORMATTED T14 IS UNFORMATTED T13 IS UNFORMATTED T12 IS UNFORMATTED WRITING TO S5,D2 THE DISK WAS COPIED SUCCESSFULLY, BUT PASSPORT DID NOT APPLY ANY PATCHES. POSSIBLE REASONS: - THE SOURCE DISK IS NOT COPY PROTECTED. - THE TARGET DISK WORKS WITHOUT PATCHES. - THE DISK USES AN UNKNOWN PROTECTION, AND PASSPORT CAN NOT HELP ANY FURTHER. --^-- The copy that Passport produces can not read itself, which is not entirely unexpected. It is most likely enforcing the custom "DA AA EB" epilogue, which is now the standard "DE AA EB". A quick sector search for "BD 8C C0" (the standard opcode to read the data latch from a drive indexed by the X register) found a DOS-shaped RWTS on track $04, which is interesting but not the code I was looking for. (My failed copy never gets off track 0.) So, putting a pin in the fact that there is a secondary RWTS that may be used later once the game is loaded, I set off to trace the boot. ~ Chapter 2 Boot Trace and Chill [S6,D1=original disk] [S5,D1=my work disk] ]PR#5 ... ]CALL -151 *9600 DE The data field parsing routine starts immediately after, at $05B0 (in memory now at $25B0): *25B0L 25B0- A6 30 LDX $30 25B2- A0 18 LDY #$18 25B4- 88 DEY 25B5- 30 F7 BMI $25AE ; match $D5 $AA $AD as normal 25B7- BD 8C C0 LDA $C08C,X 25BA- 10 FB BPL $25B7 25BC- C9 D5 CMP #$D5 25BE- D0 F4 BNE $25B4 25C0- EA NOP 25C1- BD 8C C0 LDA $C08C,X 25C4- 10 FB BPL $25C1 25C6- C9 AA CMP #$AA 25C8- D0 F2 BNE $25BC 25CA- A0 56 LDY #$56 25CC- BD 8C C0 LDA $C08C,X 25CF- 10 FB BPL $25CC 25D1- 49 AD EOR #$AD 25D3- D0 E7 BNE $25BC ; ??? 25D5- 08 PHP 25D6- 20 AC 05 JSR $05AC 25D9- 28 PLP *25ACL 25AC- 18 CLC 25AD- 60 RTS Nothing. We're calling a subroutine to do absolutely nothing except clear the carry. And since we saved and restored the status flags, we're not even doing that. Onward. 25DA- 88 DEY 25DB- 84 0B STY $0B 25DD- BC 8C C0 LDY $C08C,X 25E0- 10 FB BPL $25DD 25E2- 59 D6 02 EOR $02D6,Y 25E5- A4 0B LDY $0B 25E7- 99 00 03 STA $0300,Y 25EA- D0 EE BNE $25DA 25EC- 84 0B STY $0B 25EE- BC 8C C0 LDY $C08C,X 25F1- 10 FB BPL $25EE 25F3- 59 D6 02 EOR $02D6,Y 25F6- A4 0B LDY $0B 25F8- 99 00 02 STA $0200,Y 25FB- C8 INY 25FC- D0 EE BNE $25EC 25FE- BC 8C C0 LDY $C08C,X 2601- 10 FB BPL $25FE 2603- 59 D6 02 EOR $02D6,Y 2606- D0 A6 BNE $25AE ; also extremely weird 2608- A1 00 LDA ($00,X) ; match all three epilogue nibbles 260A- BD 8C C0 LDA $C08C,X 260D- 10 FB BPL $260A 260F- C9 DA CMP #$DA <-- ! 2611- D0 9B BNE $25AE 2613- EA NOP 2614- BD 8C C0 LDA $C08C,X 2617- 10 FB BPL $2614 2619- C9 AA CMP #$AA 261B- D0 91 BNE $25AE 261D- A4 2F LDY $2F 261F- BD 8C C0 LDA $C08C,X 2622- 10 FB BPL $261F 2624- C9 EB CMP #$EB 2626- D0 86 BNE $25AE ; finish decoding disk nibbles into ; bytes in memory 2628- A2 56 LDX #$56 262A- CA DEX 262B- 30 FB BMI $2628 262D- B9 00 02 LDA $0200,Y 2630- 5E 00 03 LSR $0300,X 2633- 2A ROL 2634- 5E 00 03 LSR $0300,X 2637- 2A ROL 2638- 91 04 STA ($04),Y 263A- C8 INY 263B- D0 ED BNE $262A 263D- 18 CLC 263E- 60 RTS OK, the most obvious patch is the data epilogue, which is now $DE $AA $EB. T00,S0D,$10: DA -> DE ]PR#6 ...reboots endlessly... Hmm. Maybe because it's enforcing the third data epilogue nibble? ; change the branch to the next line so ; it effectively ignores the third ; data epilogue nibble T00,S0D,$27: 86 -> 00 Nope, still reboots endlessly. (I'll keep the patch anyway.) Hmm. It's not the address prologue matching; the LSR/EOR code to match $D4 or $D5 is a common trick. It still matches $D5 on any track, so it shouldn't require any change. After staring at this code for longer than I would like to admit, it finally dawned on me what this weird code immediately after the data prologue is for: ; cycles counts in margin, because that ; is the most important part 25CC- BD 8C C0 LDA $C08C,X ; 4 25CF- 10 FB BPL $25CC ; 2 25D1- 49 AD EOR #$AD ; 2 25D3- D0 E7 BNE $25BC ; 2 25D5- 08 PHP ; 3 25D6- 20 AC 05 JSR $05AC ; 6 25AC- 18 CLC ; 2 25AD- 60 RTS ; 6 25D9- 28 PLP ; 4 25DA- 88 DEY ; 2 25DB- 84 0B STY $0B ; 3 25DD- BC 8C C0 LDY $C08C,X ;*3 25E0- 10 FB BPL $25DD 4+2+2+2+3+6+2+6+4+2+3+3 = 39, which is too long. Each bit on disk takes 4 CPU cycles to come around as the disk is spinning. That means we need to read an 8-bit nibble every 32 cycles. The data latch will hold the last value for 4 more cycles -- to compensate for the fact that the read is actually done on the third cycle of the 4-cycle LDA/LDY instruction that hits the data latch soft switch -- so we can spend an absolute maximum of 36 cycles fetching any one nibble. We've intentionally wasted enough time that we'll miss the first bit of the first nibble of the data field. Unless... If the $AD, the third data prologue nibble, has a timing bit after it, then the data latch would hold that value for 4 more cycles, and we would just barely have enough time to catch the first bit of the next nibble. Turning back to the Copy II Plus nibble editor, I can see it on the original disk (originally shown in inverse, which I converted to "+"): --v-- COPY ][ PLUS BIT COPY PROGRAM 8.4 (C) 1982-9 CENTRAL POINT SOFTWARE, INC. --------------------------------------- TRACK: 01 START: 36E5 LENGTH: 188C 36C0: CD 9A D3 96 96 D3 AC E6 VIEW 36C8: 97 96 9D B5 F3 96 DA AA 36D0: EB 99 BF+D9+A7+F2 FF+FF+ 36D8: B9 FF+FE+DC+FF+FF+FF+FC+ 36E0: BE+FE+FF+FF+FF+D4 AA 96 <-36E5 ^^^^^^^^ address prologue 36E8: AA AA AA AB AA AB AA AA ^^^^^ ^^^^^ ^^^^^ ^^^^^ V=000 T=$01 S=$01 chksm 36F0: DA AA EB 99 B5 FC D9 B7 ^^^^^^^^ address epilogue 36F8: F9 FE+FF+FF+D5 AA AD+FE ^^^^^^^^^ data prologue with timing bit after the third nibble --^-- At $05D5, I can put a "BEQ" to branch over the JSR, which will save enough time to make this code work on my normalized disk with no extra timing bits. T00,S0E,$D5: 0820 -> F003 ]PR#6 ...still reboots endlessly... I. Am. Still. Missing. Something. ~ Chapter 6 Secret Agent Double-O... Zero? After staring at this bootloader for an embarrassing amount of time, looking at the original disk in both a nibble and a sector editor, I found another difference: the disk volume number. The original disk has a disk volume number of 0 -- which, by the way, is impossible to create with standard tools. But as we've already seen, this disk is anything but standard. (My copy has a disk volume 254, the default.) But who cares? On an unprotected disk, or even a protected disk with a modified copy of DOS 3.3, the RWTS can check and intentionally reject a disk with the wrong disk volume number. This bootloader does not contain that check. Except... At $061D, the custom RWTS is finishing up matching the data field epilogue and getting ready to finish decoding the disk nibbles in the data field and verifying the checksum before returning a final boolean in the carry flag -- yes, this sector was read successfully, or no, it wasn't. ``'-.,_,.-'``'-.,_,.='``'-.,_,.-'``'-., ``'-.,_,.-'``'-.,_,.='``'-.,_,.-'``'-., `` ., `` 261D- A4 2F LDY $2F ., `` 261F- BD 8C C0 LDA $C08C,X ., `` 2622- 10 FB BPL $261F ., `` 2624- C9 EB CMP #$EB ., `` 2626- D0 86 BNE $25AE ., `` ., ``'-.,_,.-'``'-.,_,.='``'-.,_,.-'``'-., ``'-.,_,.-'``'-.,_,.='``'-.,_,.-'``'-., Look how we're initializing the Y register: with zero page $2F. What's in $2F? Why, we just set that, while we were parsing the address field. It's the disk volume number. The disk volume number has nothing to do with this part of the RWTS. The Y register is used to look up into the nibble translation table. In a standard DOS 3.3 RWTS, that register is always initialized to 0. So this is a very, very, very sneaky way of ensuring that the disk volume number is 0. Which, of course, mine isn't. The patch is to make the code do what DOS 3.3 does: always initialize the Y register to 0 going into the final nibble decoding. T00,S0D,$1D: A42F -> A000 ]PR#6 ...hangs with the drive motor off... This is definitely progress. I mean, I don't get to play the game or anything, but it's no longer rebooting endlessly. That means I'm now hitting an entirely different way that this copy protection is telling me to go f--- myself. ~ Chapter 7 The 4shadow Knows The RWTS now works. It is successfully reading sectors and returning to the caller. Where is the caller? Searching for references to $0562, it appears the caller is at $075F. ]PR#5 ]BLOAD OBJ.0800-0BFF DECRYPTED,A$800 ]CALL -151 *2500<900.BFFM Now $0753 is in memory at $2753. *275FL ; read and parse address field 275F- 20 62 05 JSR $0562 ; endlessly loop on error 2762- B0 FB BCS $275F ; check if we're on the right track ; (I mean literally whether the track ; number is the expected value) 2764- A5 2E LDA $2E 2766- 85 0D STA $0D 2768- C5 0F CMP $0F ; if not, branch back and recalibrate ; (not shown) 276A- D0 EE BNE $275A ; XOR with the first nibble of the ; address prologue ($D4 or $D5, set at ; $0569) 276C- 45 01 EOR $01 ; check the low bit 276E- 29 01 AND #$01 ; branch back if it doesn't match 2770- F0 ED BEQ $275F So even though the RWTS code itself accepts a $D4 or $D5 for the first address prologue nibble, the caller then goes out of its way to check which one it was. The even tracks need to be $D5 $AA $96, and the odd tracks need to be $D4 $AA $96. Which, of course, is no longer true. On my copy, every track uses the standard $D5 $AA $96. I can fool this check by changing the "EOR" instruction to an "LDA", which will ensure the "BEQ" never branches, so it will never loop back to retry a sector that already succeeded. T00,S0C,$6C: 29 -> A9 Fun fact: the only time this bootloader turns on the drive motor is in the change track routine, which immediately turn it off again. The rest of the RWTS relies on the fact that the disk drive won't actually turn off the motor when it's told to. It lingers for about a second -- long enough for the disk to spin about 5 full revolutions. The code assumes (correctly) that it can read all the sectors on a track before the drive motor actually turns off, at which point it moves to the next track, which turns the drive motor on again, and the cycle repeats. On my copy, the caller intentionally rejects sectors based on the first nibble of the address prologue, so it keeps retrying the reads even though they succeeded. Eventually the disk drive motor really does turn off, at which point nothing works, and we're stuck in an endless loop of trying to read a disk that is no longer spinning. ~ Chapter 8 Chasing That Natural High (Score) The disk still doesn't boot fully, but we are absolutely, positively, most definitely making progress. Now it loads several tracks (probably the entire game) before rebooting. Because there is AN ENTIRELY OTHER RWTS on the disk for the exclusive purpose of reading and writing out high scores, which are stored on T00,S09. This RWTS is stored in reverse order on track $04, but it looks like it gets loaded into $B800 in memory. (I actually found this RWTS by accident when I was looking for the bootloader. Talk about 4shadowing!) T04,S0F,$35: DA -> DE T04,S0F,$91: DA -> DE T04,S01,$9E: DA -> DE ]PR#6 ...works, and it is glorious... Quod erat liberandum. ~ Acknowledgments Thanks to @DiskBlitz for the original disk. --------------------------------------- A 4am crack No. 1785 ------------------EOF------------------