-----------Bouncing Kamungas----------- A 4am crack 2018-05-05 --------------------------------------- Name: Bouncing Kamungas Genre: arcade Year: 1983 Credits: Tom Becklund Publisher: Penguin Software Platform: Apple ][+ or later Media: single-sided 5.25-inch floppy OS: custom Similar cracks: #1723 The Queen of Phobos #1697 Crime Wave #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 "At Least You Tried" Locksmith Fast Disk Backup unable to read any track EDD 4 bit copy to .nib file on CFFA3000 read errors on T0B+ copy fails to boot, hangs with the drive motor off Copy ][+ nibble editor tracks $0B-$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 $0A 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 The 2017-12-27 development version of 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. All in all, I have high confidence in the integrity of this conversion. 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 T11 IS UNFORMATTED T10 IS UNFORMATTED T0F IS UNFORMATTED T0E IS UNFORMATTED T0D IS UNFORMATTED T0C IS UNFORMATTED T0B IS UNFORMATTED WRITING TO S5,D2 THE DISK WAS COPIED SUCCESSFULLY, BUT PASSPORT DID NOT APPLY ANY PATCHES. --^-- 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 $08, 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 $05A2 (in memory now at $25A2): *25A2L 25A2- A6 30 LDX $30 25A4- A0 18 LDY #$18 25A6- 88 DEY 25A7- 30 F7 BMI $25A0 ; match $D5 $AA $AD as normal 25A9- BD 8C C0 LDA $C08C,X 25AC- 10 FB BPL $25A9 25AE- C9 D5 CMP #$D5 25B0- D0 F4 BNE $25A6 25B2- EA NOP 25B3- BD 8C C0 LDA $C08C,X 25B6- 10 FB BPL $25B3 25B8- C9 AA CMP #$AA 25BA- D0 F2 BNE $25AE 25BC- A0 56 LDY #$56 25BE- BD 8C C0 LDA $C08C,X 25C1- 10 FB BPL $25BE 25C3- 49 AD EOR #$AD 25C5- D0 E7 BNE $25AE ; ??? 25C7- 08 PHP 25C8- 20 9E 05 JSR $059E 25CB- 28 PLP *259EL 259E- 18 CLC 259F- 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. *25CCL 25CC- 88 DEY 25CD- 84 0B STY $0B 25CF- BC 8C C0 LDY $C08C,X 25D2- 10 FB BPL $25CF 25D4- 59 D6 02 EOR $02D6,Y 25D7- A4 0B LDY $0B 25D9- 99 00 03 STA $0300,Y 25DC- D0 EE BNE $25CC 25DE- 84 0B STY $0B 25E0- BC 8C C0 LDY $C08C,X 25E3- 10 FB BPL $25E0 25E5- 59 D6 02 EOR $02D6,Y 25E8- A4 0B LDY $0B 25EA- 99 00 02 STA $0200,Y 25ED- C8 INY 25EE- D0 EE BNE $25DE 25F0- BC 8C C0 LDY $C08C,X 25F3- 10 FB BPL $25F0 25F5- 59 D6 02 EOR $02D6,Y 25F8- D0 A6 BNE $25A0 ; also extremely weird 25FA- A1 00 LDA ($00,X) ; match all three epilogue nibbles 25FC- BD 8C C0 LDA $C08C,X 25FF- 10 FB BPL $25FC 2601- C9 DA CMP #$DA <-- ! 2603- D0 9B BNE $25A0 2605- EA NOP 2606- BD 8C C0 LDA $C08C,X 2609- 10 FB BPL $2606 260B- C9 AA CMP #$AA 260D- D0 91 BNE $25A0 260F- A4 2F LDY $2F 2611- BD 8C C0 LDA $C08C,X 2614- 10 FB BPL $2611 2616- C9 EB CMP #$EB 2618- D0 86 BNE $25A0 ; finish decoding disk nibbles into ; bytes in memory 261A- A2 56 LDX #$56 261C- CA DEX 261D- 30 FB BMI $261A 261F- B9 00 02 LDA $0200,Y 2622- 5E 00 03 LSR $0300,X 2625- 2A ROL 2626- 5E 00 03 LSR $0300,X 2629- 2A ROL 262A- 91 04 STA ($04),Y 262C- C8 INY 262D- D0 ED BNE $261C 262F- 18 CLC 2630- 60 RTS OK, the most obvious patch is the first data epilogue nibble, which is now $DE. T00,S0D,$02: 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,$19: 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 25DE- 84 0B STY $0B *25BEL 25BE- BD 8C C0 LDA $C08C,X ; 4 25C1- 10 FB BPL $25BE ; 2 25C3- 49 AD EOR #$AD ; 2 25C5- D0 E7 BNE $25AE ; 2 25C7- 08 PHP ; 3 25C8- 20 9E 05 JSR $059E ; 6 259E- 18 CLC ; 2 259F- 60 RTS ; 6 25CB- 28 PLP ; 4 25CC- 88 DEY ; 2 25CD- 84 0B STY $0B ; 3 25CF- BC 8C C0 LDY $C08C,X ;*3 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: 200D LENGTH: 18C7 1FE8: ED BB FE FC ED FF FC ED VIEW 1FF0: BB BB FC FC ED BB FE ED 1FF8: BB FC FC ED BB BB FC EC 2000: EC FC FC ED BB FC ED BB 2008: FC FE FF FF FF D5 AA 96 ^^^^^^^^ address prologue 2010: AA AA AA AB AA AA AA AB 2018: DA AA EB 99 FF+FF+FF+FB+ ^^^^^^^^ address epilogue 2020: FF+FF+FF+FF+D5 AA AD+A6 ^^^^^^^^ data prologue with timing bit after the third nibble --^-- At $05C7, 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,$C7: 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 subtle difference: the disk volume number. The original disk has a disk volume number of 000 -- 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 $060F, 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. ``'-.,_,.-'``'-.,_,.='``'-.,_,.-'``'-., ``'-.,_,.-'``'-.,_,.='``'-.,_,.-'``'-., `` ., `` 260F- A4 2F LDY $2F ., `` 2611- BD 8C C0 LDA $C08C,X ., `` 2614- 10 FB BPL $2611 ., `` 2616- C9 EB CMP #$EB ., `` 2618- D0 86 BNE $25A0 ., `` ., ``'-.,_,.-'``'-.,_,.='``'-.,_,.-'``'-., ``'-.,_,.-'``'-.,_,.='``'-.,_,.-'``'-., 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,$0F: 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 $0554, it appears the caller is at $0753. ]PR#5 ]BLOAD OBJ.0800-0BFF DECRYPTED,A$800 ]CALL -151 *2500<900.BFFM Now $0753 is in memory at $2753. *2753L ; read and parse address field 2753- 20 54 05 JSR $0554 ; endlessly loop on error 2756- B0 FB BCS $2753 ; check if we're on the right track ; (I mean literally whether the track ; number is the expected value) 2758- A5 2E LDA $2E 275A- 85 0D STA $0D 275C- C5 0F CMP $0F ; if not, branch back and recalibrate ; (not shown) 275E- D0 EE BNE $274E ; get the physical sector number ; (just parsed from the address field) 2760- A6 2D LDX $2D 2762- 8A TXA ; XOR with the first nibble of the ; address prologue ($D4 or $D5, set at ; $055B) 2763- 45 01 EOR $01 <-- ! ; check the low bit 2765- 29 01 AND #$01 ; branch back if it doesn't match 2767- F0 EA BEQ $2753 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 sectors need to be $D5 $AA $96, and the odd sectors need to be $D4 $AA $96. Which, of course, is no longer true. On my copy, every sector 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,$65: 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 game boots. The game loads. The game plays. Then you get a high score, you enter your initials, and they appear in the high score list. Except it doesn't actually save them. After you reboot, the high score list is empty again. Because there is AN ENTIRELY OTHER RWTS on the disk for the exclusive purpose of writing out high scores. It's stored in a weird order on track $08, but it looks like it gets loaded into $B800 in memory. T08,S08,$35: DA -> DE T08,S08,$91: DA -> DE T08,S09,$9E: DA -> DE ]PR#6 ...works, and it is glorious... Quod erat liberandum. --------------------------------------- A 4am crack No. 1748 ------------------EOF------------------