-----------------Biomes---------------- A 4am crack 2017-09-15 --------------------------------------- Name: Biomes Genre: educational Year: 1986 Credits: developed by John Boeschen and Co.; programmers: Jon Newhall, Steve A. Baker; artist: John Grandy; consultant: Jeffrey Kaufmann Publisher: D.C. Heath and Company Platform: Apple ][+ or later (64K) Media: single-sided 5.25-inch floppy OS: ProDOS 1.1.1 Previous cracks: none ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways COPYA read error on first pass Locksmith Fast Disk Backup unable to copy track $03; copy boots ProDOS then hangs with drive motor on EDD 4 bit copy (no sync, no count) no errors, but copy boots ProDOS then says "PLACE THE BIOMES DISK IN A FLOPPY DRIVE AND RESTART THE PROGRAM" Passport finds "nibble count protection" on track 3 but doesn't apply any patches Copy ][+ nibble editor track 3 is almost entirely sync bytes --v-- COPY ][ PLUS BIT COPY PROGRAM 8.4 (C) 1982-9 CENTRAL POINT SOFTWARE, INC. --------------------------------------- TRACK: 03 START: 1800 LENGTH: 3DFF 27F0: FF FF FF FF FF FF FF FF VIEW 27F8: FF FF FF FF FF FF FF FF 2800: FF FF FF FF FF FF FF FF 2808: FF FF FF FF FF FF FF FF 2810: D5 FF FF FF FF FF FF FF <-2810 2818: FF FF FF FF FF FF FF FF 2820: FF FF FF FF FF FF FF FF 2828: FF FF FF FF FF FF FF FF 2830: FF FF FF FF FF FF FF FF --^-- Disk Fixer track 0 looks like standard ProDOS bootloader, including disk catalog; no way to read track 3 (no sectors) Why didn't COPYA work? whole-track protection sequence on track 3 Why didn't Locksmith FDB work? ditto Why didn't my EDD copy work? a runtime check (probably in the startup program) that's examining the specially formatted track 3 Next steps: 1. Trace the startup program 2. Disable the protection check 3. Declare victory (*) (*) Go to the gym ~ Chapter 1 In Which We Will Not Be Going To The Gym Any Time Soon The disk presents as a standard ProDOS disk, so let's take advantage of that. [S6,D1=non-working copy] [S7,D1=my ProDOS hard drive /A4AMCRACK] ]PR#7 ... ]CAT,S6,D1 /PROBIO NAME TYPE BLOCKS MODIFIED *PRODOS SYS 30 18-SEP-84 TMS.OBJ BIN 10 20-MAR-86 TERRAIN.PIX BIN 13 16-FEB-86 BIO.SYSTEM SYS 27 21-MAR-86 FONTSET.BLOCK BIN 3 28-OCT-85 MISSION.OBJ BIN 12 21-MAR-86 BRIEF.OBJ BIN 11 21-MAR-86 SIM.OBJ BIN 7 21-MAR-86 EARTH.PD BIN 10 27-JAN-86 LOGO BIN 17 11-OCT-85 CONGRAT BIN 12 9-DEC-85 TIME BIN 14 9-DEC-85 BASECMDR BIN 17 10-DEC-85 EXCUSEME BIN 11 8-DEC-85 HIRES.TBL BIN 1 19-JAN-86 ROSTER BIN 6 6-MAR-86 HIRES.SOURCE TXT 5 19-JAN-86 EARTH.PKED BIN 8 9-FEB-86 BLOCKS FREE: 51 BLOCKS USED: 229 ProDOS loads the first .SYSTEM file at address $2000 and transfers control to it. ]BLOAD BIO.SYSTEM,A$2000,TSYS ]CALL -151 *2000L 2000- 4C 17 52 JMP $5217 *5217L ; munge reset vector 5217- 4E F4 03 LSR $03F4 ; see below (shifts code in memory) 521A- 20 20 52 JSR $5220 ; jump to new entry point 521D- 4C 6D 1D JMP $1D6D ; shift entire program into slightly ; lower memory (necessary because you ; don't get to choose where a .SYSTEM ; file is loaded -- it's always $2000) 5220- A2 32 LDX #$32 5222- F0 08 BEQ $522C 5224- A0 FF LDY #$FF 5226- 20 2E 52 JSR $522E 5229- CA DEX 522A- D0 F8 BNE $5224 522C- A0 16 LDY #$16 522E- B9 00 20 LDA $2000,Y 5231- 99 67 1D STA $1D67,Y 5234- 88 DEY 5235- C0 FF CPY #$FF 5237- D0 F5 BNE $522E 5239- EE 30 52 INC $5230 523C- EE 33 52 INC $5233 523F- 60 RTS The copy routine is self-contained. I can run it from the monitor without losing control. *5220G *1D6DL 1D6D- 4E 70 1D LSR $1D70 1D70- DC ??? 1D71- 76 1D ROR $1D,X Well this is unpleasant: self-modifying mode. The instruction at $1D6D modifies the instruction at $1D70. The 6502 CPU had neither an instruction cache nor execution-only memory protection. You could literally change the very next instruction before executing it. Why? Because it makes it that much more difficult to reverse engineer it. Which tells me I'm on the right track. I can reproduce the self-modification elsewhere in memory to maintain control while doing the thing someone doesn't want me to be doing. ; "LSR $1D70 / RTS" *300:4E 70 1D 60 *300G *1D70L 1D70- 6E 76 1D ROR $1D76 1D73- 6E 78 1D ROR $1D78 1D76- 40 RTI 1D77- C9 3E CMP #$3E Again with the self-modifying code. ; "ROR $1D76 / ROR $1D78 / RTS" *300:6E 76 1D 6E 78 1D 60 *300G *1D76L 1D76- 20 C9 1F JSR $1FC9 *1FC9L 1FC9- 6E CC 1F ROR $1FCC 1FCC- DC ??? 1FCD- D5 1F CMP $1F,X And again. ; "ROR $1FCC / RTS" *300:6E CC 1F 60 *300G *1FCCL 1FCC- 6E D5 1F ROR $1FD5 1FCF- 6E D8 1F ROR $1FD8 1FD2- A0 21 LDY #$21 1FD4- 98 TYA 1FD5- B3 ??? 1FD6- DE 1F 32 DEC $321F,X And again. ; "ROR $1FD5 / ROR $1FD8 / RTS" *300:6E D5 1F 6E D8 1F 60 *300G *1FD2L 1FD2- A0 21 LDY #$21 1FD4- 98 TYA 1FD5- 59 DE 1F EOR $1FDE,Y 1FD8- 99 DE 1F STA $1FDE,Y 1FDB- 88 DEY 1FDC- 10 F6 BPL $1FD4 Now a loop that progressively decrypts the code immediately following the loop (which works, because no caching). ; above loop + "RTS" *300:A0 21 98 59 DE 1F 99 DE 1F 88 10 F6 60 *300G *1FDEL 1FDE- A9 AA LDA #$AA 1FE0- A2 02 LDX #$02 1FE2- F0 08 BEQ $1FEC 1FE4- A0 FF LDY #$FF 1FE6- 20 EE 1F JSR $1FEE 1FE9- CA DEX 1FEA- D0 F8 BNE $1FE4 1FEC- A0 4F LDY #$4F 1FEE- 59 79 1D EOR $1D79,Y 1FF1- 99 79 1D STA $1D79,Y 1FF4- 88 DEY 1FF5- C0 FF CPY #$FF 1FF7- D0 F5 BNE $1FEE 1FF9- EE F0 1F INC $1FF0 1FFC- EE F3 1F INC $1FF3 1FFF- 60 RTS Another progressive decryption loop, decrypting the code immediately after the JSR (at $1D76) that got us here in the first place! It's also the last thing in this subroutine, so I can execute it in place and return to the monitor. *1FDEG Now we return to $1D79, which -- not coincidentally -- we just decrypted from within the subroutine we called at $1D76. My brain hurts. ~ Chapter 2 Ow My Brain *1D79L ; loop through ProDOS devices (looking ; for disks) 1D79- AC 31 BF LDY $BF31 1D7C- AD 30 BF LDA $BF30 1D7F- 59 32 BF EOR $BF32,Y 1D82- 29 F0 AND #$F0 1D84- F0 04 BEQ $1D8A ---+ 1D86- 88 DEY | 1D87- 10 F3 BPL $1D7C | | ; should never reach here | 1D89- 00 BRK | | 1D8A- AE 31 BF LDX $BF31 <--+ 1D8D- 20 B6 1D JSR $1DB6 *1DB6L 1DB6- B9 32 BF LDA $BF32,Y 1DB9- 29 0F AND #$0F 1DBB- C9 00 CMP #$00 1DBD- 60 RTS Continuing from $1D90... 1D90- D0 13 BNE $1DA5 ; save X, Y, and zero page $01 1D92- 98 TYA 1D93- 48 PHA 1D94- 8A TXA 1D95- 48 PHA 1D96- A5 01 LDA $01 1D98- 48 PHA 1D99- 20 C4 1D JSR $1DC4 *1DC4L ; seek drive using a raw block read ; (ProDOS MLI command $80) 1DC4- B9 32 BF LDA $BF32,Y 1DC7- 29 F0 AND #$F0 1DC9- 8D BF 1D STA $1DBF 1DCC- 20 00 BF JSR $BF00 1DCF- [80 BE 1D] 1DD2- F0 02 BEQ $1DD6 1DD4- 38 SEC 1DD5- 60 RTS ; if the block read worked, save even ; more stuff on the stack 1DD6- A5 00 LDA $00 1DD8- 48 PHA 1DD9- AD BF 1D LDA $1DBF 1DDC- 29 70 AND #$70 1DDE- 85 00 STA $00 1DE0- AA TAX ; turn on drive motor manually (!) 1DE1- BD 89 C0 LDA $C089,X ; not shown, but this moves the drive ; head one track (very clever -- we ; couldn't do a block read on track 3 ; because there is no structure on the ; track, so we let ProDOS seek to the ; neighboring track then move the drive ; head manually) 1DE4- A0 04 LDY #$04 1DE6- A9 06 LDA #$06 1DE8- 20 F3 1E JSR $1EF3 1DEB- 20 8E 1E JSR $1E8E *1E8EL ; find $D5 nibble 1E8E- BD 8C C0 LDA $C08C,X 1E91- 10 FB BPL $1E8E 1E93- 48 PHA 1E94- 68 PLA 1E95- C9 D5 CMP #$D5 1E97- D0 F5 BNE $1E8E ; initialize a checksum 1E99- A0 00 LDY #$00 1E9B- 84 01 STY $01 ; count number of $F7 nibbles before ; another $D5 nibble 1E9D- BD 8C C0 LDA $C08C,X 1EA0- 10 FB BPL $1E9D 1EA2- C9 D5 CMP #$D5 1EA4- F0 0D BEQ $1EB3 1EA6- C9 F7 CMP #$F7 1EA8- D0 01 BNE $1EAB 1EAA- C8 INY ; the sum of the nibbles themselves ; constitutes the checksum 1EAB- 18 CLC 1EAC- 65 01 ADC $01 1EAE- 85 01 STA $01 1EB0- 4C 9D 1E JMP $1E9D 1EB3- 98 TYA 1EB4- F0 E3 BEQ $1E99 ; skip $FF nibbles 1EB6- BD 8C C0 LDA $C08C,X 1EB9- 10 FB BPL $1EB6 1EBB- 48 PHA 1EBC- 68 PLA 1EBD- C9 FF CMP #$FF 1EBF- F0 F5 BEQ $1EB6 ; if next nibble is $D5, fail 1EC1- C9 D5 CMP #$D5 1EC3- F0 2C BEQ $1EF1 ; skip several more nibbles 1EC5- A0 05 LDY #$05 1EC7- BD 8C C0 LDA $C08C,X 1ECA- 10 FB BPL $1EC7 1ECC- 48 PHA 1ECD- 68 PLA 1ECE- 88 DEY 1ECF- D0 F6 BNE $1EC7 ; skip $FF nibbles 1ED1- BD 8C C0 LDA $C08C,X 1ED4- 10 FB BPL $1ED1 1ED6- 48 PHA 1ED7- 68 PLA 1ED8- C9 FF CMP #$FF 1EDA- F0 F5 BEQ $1ED1 ; if next nibble is not $D5, fail 1EDC- C9 D5 CMP #$D5 1EDE- D0 11 BNE $1EF1 ; skip $FF nibbles 1EE0- BD 8C C0 LDA $C08C,X 1EE3- 10 FB BPL $1EE0 1EE5- C9 FF CMP #$FF 1EE7- D0 08 BNE $1EF1 ; verify checksum, branch on failure 1EE9- A5 01 LDA $01 1EEB- C9 10 CMP #$10 1EED- D0 02 BNE $1EF1 ; success path falls through to here -- ; clear carry and return 1EEF- 18 CLC 1EF0- 60 RTS ; failure path is here -- set carry and ; return 1EF1- 38 SEC 1EF2- 60 RTS Continuing from $1DEE... ; save flags 1DEE- 08 PHP ; restore drive head position -- moving ; back to the track ProDOS thought we ; never left 1DEF- A0 06 LDY #$06 1DF1- A9 04 LDA #$04 1DF3- 20 F3 1E JSR $1EF3 ; restore flags 1DF6- 28 PLP ; turn off drive motor and exit (with ; at least the carry flag intact) 1DF7- A6 00 LDX $00 1DF9- BD 88 C0 LDA $C088,X 1DFC- 68 PLA 1DFD- 85 00 STA $00 1DFF- 60 RTS Now continuing from $1D9C... *1D9CL ; restore everything we saved earlier 1D9C- 68 PLA 1D9D- 85 01 STA $01 1D9F- 68 PLA 1DA0- AA TAX 1DA1- 68 PLA 1DA2- A8 TAY ; carry clear = protection check ; succeeded, so branch to success path 1DA3- 90 0E BCC $1DB3 ; otherwise try another slot/drive 1DA5- CA DEX 1DA6- 30 08 BMI $1DB0 1DA8- 88 DEY 1DA9- 10 E2 BPL $1D8D 1DAB- AC 31 BF LDY $BF31 1DAE- 10 DD BPL $1D8D ; ultimate failure -- jump to The ; Badlands (not shown, but trust me, ; it's bad) 1DB0- 4C 00 1E JMP $1E00 ; execution continues here (from $1DA3) 1DB3- 4C 84 1F JMP $1F84 *1F84L 1F84- 20 8D 1F JSR $1F8D *1F8DL ; wipe all the copy protection code ; from memory 1F8D- A2 02 LDX #$02 1F8F- F0 08 BEQ $1F99 1F91- A0 FF LDY #$FF 1F93- 20 9B 1F JSR $1F9B 1F96- CA DEX 1F97- D0 F8 BNE $1F91 1F99- A0 1C LDY #$1C 1F9B- 99 67 1D STA $1D67,Y 1F9E- 88 DEY 1F9F- C0 FF CPY #$FF 1FA1- D0 F8 BNE $1F9B 1FA3- EE 9D 1F INC $1F9D 1FA6- 60 RTS Continuing from $1F87... 1F87- 20 A7 1F JSR $1FA7 *1FA7L ; yet another progressive decryption ; loop, for the actual program code 1FA7- A9 AA LDA #$AA 1FA9- A2 2F LDX #$2F 1FAB- F0 08 BEQ $1FB5 1FAD- A0 FF LDY #$FF 1FAF- 20 B7 1F JSR $1FB7 1FB2- CA DEX 1FB3- D0 F8 BNE $1FAD 1FB5- A0 7D LDY #$7D 1FB7- 59 00 20 EOR $2000,Y 1FBA- 99 00 20 STA $2000,Y 1FBD- 88 DEY 1FBE- C0 FF CPY #$FF 1FC0- D0 F5 BNE $1FB7 1FC2- EE B9 1F INC $1FB9 1FC5- EE BC 1F INC $1FBC 1FC8- 60 RTS Continuing from $1F8A... ; start the actual program (now ; decrypted) 1F8A- 4C 00 20 JMP $2000 Interesting: the protection check is designed to poll all available floppy drives. That's why the error message is phrased the way it is, "PLACE THE DISK IN *A* FLOPPY DRIVE..." It doesn't care which one. You could copy the program files to a hard drive and run it from there. As long as your original program disk was in a floppy drive, the loop at $1D79 would eventually find it and the protection check would pass. (Counterpoint: if you have another ProDOS disk in a floppy drive and it finds that one first, the code at $1E8E will hang forever looking for the proper stream on track 3. That's what happened with my sector copy.) I wouldn't call any protection scheme "user friendly," but this one does intentionally enable certain use cases. Now let's burn it to the ground. ~ Chapter 3 A Light Touch On the bright side, the protection check does not appear to be integrated with the rest of the program. It zeroes itself out, decrypts the main program, and is never heard from again. So I've got that going for me, which is nice. On the less-than-bright side, I can't skip the protection check altogether because the main program is encrypted on disk and decrypted by a routine that runs after the protection check but is itself decrypted by a separate routine that runs before the protection check. It's non-trivial to patch the check itself because of the progressive encryption, so where does that leave us? One heavy-handed solution would be to save out the decrypted program code to a new .SYSTEM file. It even starts at $2000, which is where all .SYSTEM files are loaded anyway. A less invasive approach would be to leave the protection intact but never call it. The initial code at $5217 exited via $1D6D, which decrypts and calls the protection check. My replacement routine could call $1FC9 directly (to decrypt the protection code and the success path), then exit via $1F84 (the success path). The only caveat is the code at $1FC9 is self-modifying and assumes the carry flag is clear on entry. (The first modification is a ROR, which rotates the carry into the byte value.) Thus: *BLOAD BIO.SYSTEM,A$2000,TSYS *521E:40 52 *5240:18 20 C9 1F 4C 84 1F *5217L 5217- 4E F4 03 LSR $03F4 521A- 20 20 52 JSR $5220 521D- 4C 40 52 JMP $5240 --+ ... | 5240- 18 CLC <-+ 5241- 20 C9 1F JSR $1FC9 5244- 4C 84 1F JMP $1F84 *BSAVE BIO.SYSTEM,A$2000,L12871,TSYS ]PR#6 ...works, and it is glorious... Quod erat liberandum. --------------------------------------- A 4am crack No. 1424 ------------------EOF------------------