------------The Quarter Mile----------- A 4am crack 2020-01-09 --------------------------------------- Name: The Quarter Mile Version: 4.0 Genre: educational Year: 1992 Publisher: Barnum Software Platform: Apple ][ with 3.5-inch drive Media: 3.5-inch disk Disks: 1 OS: ProDOS 1.7 Previous cracks: none Similar cracks: #2032 Troll Sports Math ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways Copy ][+ 9.1 ("COPY" > "DISK") no read errors, but copy loads ProDOS then crashes at $C505 CFFA 3000 import no read errors, but booting the disk image in an emulator exhibits the same behavior as the backup I made on real hardware with Copy ][+ I do not know if the original disk boots. The slim amount of documentation I received with the disk states that it requires a IIgs, //c+, or a //e with a UniDisk 3.5 drive, which I do not have. (I have an Apple //e with a SuperDrive in slot 2, which is admittedly pushing it since even some unprotected 3.5-inch disks require booting from slot 5.) However, legitimate disk read failures do not tend to crash, so I'm guessing this is the failure mode of a run-time protection check. Next steps: 1. Trace the startup program 2. Find and disable the protection check 3. Declare victory(*) (*) go to the gym ~ Chapter 1 In Which Things Quickly Get Hairy The disk presents as standard ProDOS, with a readable disk catalog. ]CAT,S2,D2 ]CATALO\ ]CAT,S7,d2 /QUARTER.MILE NAME TYPE BLOCKS MODIFIED PRODOS SYS 32 22-MAR-89 COM.SYSTEM SYS 8 1-JUN-92 TP BIN 17 15-APR-92 PROGA BIN 32 29-APR-92 P BIN 38 28-APR-92 KEYBOARDING. DIR 1 2-NOV-96 WHOLE.NUMBERS. DIR 2 2-NOV-96 FRACTION.INTRO. DIR 1 5-NOV-96 FRACTIONS. DIR 1 DECIMALS. DIR 1 1-JUN-92 PERCENTS. DIR 1 INTEGERS. DIR 1 EQUATIONS. DIR 1 21-SEP-93 BLOCKS FREE: 913 BLOCKS USED: 687 ]PREFIX /QUARTER.MILE ]BLOAD COM.SYSTEM,A$2000,TSYS ]CALL-151 *2000L 2000- 6C 7A 2C JMP ($2C7A) *2C7A.2C7B 2C7A- 24 2C *2C24L ; save flags 2C24- 08 PHP 2C25- D8 CLD ; check for Apple IIgs 2C26- 38 SEC 2C27- 20 1F FE JSR $FE1F 2C2A- B0 02 BCS $2C2E ; IIgs-specific instruction (SEP #$30) ; which forces some bits in the status ; register to 8-bit mode 2C2C- [E2 30] ; lightly obfuscated code here, which ; I've taken the liberty of lightly ; de-obfuscating 2C2E- A0 00 LDY #$00 2C30- 84 06 STY $06 2C32- F0 01 BEQ $2C35 2C34- [AF] 2C35- A0 00 LDY #$00 2C37- A9 20 LDA #$20 2C39- D0 01 BNE $2C3C 2C3B- [5C] 2C3C- 85 07 STA $07 2C3E- D0 01 BNE $2C41 2C40- [22] 2C41- 20 74 2C JSR $2C74 *2C74L ; get a byte and "decrypt" (XOR) it 2C74- B1 06 LDA ($06),Y 2C76- 49 FC EOR #$FC 2C78- 38 SEC 2C79- 60 RTS ; always branches 2C44- B0 01 BCS $2C47 2C46- [43] ; store decrypted byte in place 2C47- 91 06 STA ($06),Y 2C49- C8 INY 2C4A- D0 F5 BNE $2C41 ; always branches 2C4C- F0 01 BEQ $2C4F 2C4E- [C7] ; decrypt more pages 2C4F- E6 07 INC $07 2C51- D0 01 BNE $2C54 2C53- [22] ; until $2C00 2C54- A5 07 LDA $07 2C56- C9 2C CMP #$2C 2C58- D0 E7 BNE $2C41 2C5A- F0 01 BEQ $2C5D 2C5C- [13] ; even more 2C5D- 20 74 2C JSR $2C74 2C60- B0 01 BCS $2C63 2C62- [27] 2C63- 91 06 STA ($06),Y 2C65- C8 INY ; until $2C23 2C66- C0 23 CPY #$23 2C68- D0 F3 BNE $2C5D ; always branches 2C6A- F0 01 BEQ $2C6D 2C6C- [3C] ; execute continues here 2C6D- 6C 71 2C JMP ($2C71) That was a lot of work to decrypt what I can only assume is the protection routine. *2C71.2C72 2C71- 00 28 That's in the code we just decrypted, so let's do that. ; RTS instead of JMP *2C6D:60 ; execute the decryption loops (without ; the initial PHP) *2C2EG Piece of cake. ~ Chapter 2 In Which It Is Most Definitely Not A Piece Of Cake And The Author Would Appreciate It If He Would Stop Calling It That Let's see what wonderous code awaits us after all that obfuscation, decryption, and indirection. *2800L ; mangle the reset vector, so you know ; this is getting esrious 2800- A0 10 LDY #$10 2802- 8C F4 03 STY $03F4 ; hard-coded to assume we're booting ; from slot 5, and self-modify some ; code later 2805- AD FF C5 LDA $C5FF 2808- 18 CLC 2809- 69 03 ADC #$03 280B- 8D 9A 28 STA $289A ; take ProDOS boot slot/drive and ; store it 280E- AD 30 BF LDA $BF30 2811- 8D 5F 29 STA $295F ; check high bit of last-accessed drive ; (0 = drive 1, 1 = drive 2) 2814- 29 80 AND #$80 2816- 0A ASL 2817- 90 17 BCC $2830 ; this protection check supports ; launching from drive 2 (but again, ; only from slot 5, because f--- you) 2819- A9 02 LDA #$02 281B- 8D 58 29 STA $2958 281E- 8D 65 29 STA $2965 2821- 8D 6C 29 STA $296C 2824- 8D 77 29 STA $2977 2827- 8D 7F 29 STA $297F 282A- 8D 84 29 STA $2984 282D- 8D 90 29 STA $2990 ; save page 3 vectors 2830- A0 00 LDY #$00 2832- B9 D0 03 LDA $03D0,Y 2835- 99 F2 2B STA $2BF2,Y 2838- C8 INY 2839- C0 31 CPY #$31 283B- D0 F5 BNE $2832 ; MLI command $80 (raw block read) ; with parameter block at $295E 283D- 20 00 BF JSR $BF00 2840- [80] 2841- [5E 29] *295E. 295E- .. .. .. .. .. .. 03 50 2960- 00 02 00 00 So we're reading block 0 into $0200. (The slot/drive at $295F was self- modified earlier.) Continuing from $2843... ; check if a page 3 vector has been ; modified (not sure what would cause ; this) 2843- AD F9 03 LDA $03F9 2846- CD C8 2B CMP $2BC8 ; if modified, fail immediately 2849- D0 2B BNE $2876 *2876L 2876- 20 58 FC JSR $FC58 2879- A9 0C LDA #$0C 287B- 85 25 STA $25 287D- 20 22 FC JSR $FC22 2880- A9 03 LDA #$03 2882- 85 24 STA $24 2884- A0 00 LDY #$00 2886- B9 CA 2B LDA $2BCA,Y 2889- C9 00 CMP #$00 288B- F0 06 BEQ $2893 288D- 20 ED FD JSR $FDED 2890- C8 INY 2891- D0 F3 BNE $2886 --v-- "THIS IS THE INCORRECT PROGRAM DISK" --^-- Well yes, but actually no. Continuing from $284B... ; restore page 3 vectors 284B- A0 00 LDY #$00 284D- B9 F2 2B LDA $2BF2,Y 2850- 99 D0 03 STA $03D0,Y 2853- C8 INY 2854- C0 31 CPY #$31 2856- D0 F5 BNE $284D ; check for IIgs 2858- 38 SEC 2859- 20 1F FE JSR $FE1F ; IIgs branches 285C- 90 3E BCC $289C ; further checks for different 8-bit ; models -- see Tech Note 7 "Apple II ; Family Identification" 285E- AD B3 FB LDA $FBB3 2861- C9 06 CMP #$06 ; Apple ][, ][+, and /// will branch 2863- D0 31 BNE $2896 2865- AD C0 FB LDA $FBC0 2868- C9 00 CMP #$00 ; Apple //e, //e enhanced will branch 286A- D0 2A BNE $2896 286C- AD BF FB LDA $FBBF 286F- C9 05 CMP #$05 ; Apple //c will branch 2871- D0 23 BNE $2896 ; Apple //c+ is pretty much the only ; model left at this point 2873- 4C 22 29 JMP $2922 So, three paths: IIgs -> $289C ][, ][+, //e, /// -> $2896 //c+ -> $2922 Since I'm on a //e, I'll focus on that path. *2896L 2896- 4C B4 28 JMP $28B4 *28B4L ; call a routine that takes parameters ; on the stack, branch to $28D5 if it ; fails (more on this in a moment) 28B4- 20 99 28 JSR $2899 28B7- [04] 28B8- [76 29] 28BA- B0 19 BCS $28D5 ; again, but different parameters 28BC- 20 99 28 JSR $2899 28BF- [04] 28C0- [7E 29] 28C2- B0 11 BCS $28D5 ; again 28C4- 20 99 28 JSR $2899 28C7- [04] 28C8- [83 29] 28CA- B0 09 BCS $28D5 ; again 28CC- 20 99 28 JSR $2899 28CF- [01] 28D0- [57 29] ; all done 28D2- 4C 22 29 JMP $2922 *2922L ; copy this code to lower memory 2922- A0 10 LDY #$10 2924- B9 B7 2B LDA $2BB7,Y 2927- 99 00 20 STA $2000,Y 292A- 88 DEY 292B- 10 F7 BPL $2924 292D- A0 1B LDY #$1B 292F- B9 3B 29 LDA $293B,Y 2932- 99 00 02 STA $0200,Y 2935- 88 DEY 2936- 10 F7 BPL $292F ; and execute it from there 2938- 4C 00 02 JMP $0200 ; wipe the decrypted protection code 293B- A0 00 LDY #$00 293D- A9 00 LDA #$00 293F- 99 00 28 STA $2800,Y 2942- 99 00 29 STA $2900,Y 2945- 99 00 2A STA $2A00,Y 2948- 99 00 2B STA $2B00,Y 294B- 99 00 2C STA $2C00,Y 294E- C8 INY 294F- D0 EE BNE $293F ; restore flags (pushed at $2C24) 2951- 28 PLP ; continue with the real program code 2952- 4C 00 20 JMP $2000 That's the success path. But if any of the calls to $2899 fail, we end up at $28D5, which seems bad: ; The Badlands 28D5- A9 00 LDA #$00 28D7- 8D 03 29 STA $2903 28DA- A9 C5 LDA #$C5 28DC- 8D 04 29 STA $2904 ; relocate to lower memory 28DF- A0 18 LDY #$18 28E1- B9 ED 28 LDA $28ED,Y 28E4- 99 00 02 STA $0200,Y 28E7- 88 DEY 28E8- 10 F7 BPL $28E1 ; and continue there 28EA- 4C 00 02 JMP $0200 ; [executed from $0200] ; wipe all of main memory 28ED- A0 00 LDY #$00 28EF- A9 08 LDA #$08 28F1- 84 06 STY $06 28F3- 85 07 STA $07 28F5- 91 06 STA ($06),Y 28F7- C8 INY 28F8- D0 FB BNE $28F5 28FA- E6 07 INC $07 28FC- A5 07 LDA $07 28FE- C9 C0 CMP #$C0 2900- D0 F3 BNE $28F5 ; forever 2902- 4C 00 02 JMP $0200 So we're doing a thing at $2899, four times but with different parameters, and if they all work, we clean up and continue to the real program code. Let's see what we're doing at $2899. 2899- 4C 03 C5 JMP $C503 The jump address at $2899 was self- modified earlier as $C5FF + #$03. That would make it the entry point to the SmartPort firmware. So that's great. ~ Chapter 3 In Which Everyone Is Smart In Their Own Way SmartPort firmware is documented in "Apple IIgs Firmware Reference," ch. 7. --v-- This is an example of a standard SmartPort call: ; Call SmartPort command dispatcher SP_CALL JSR DISPATCH ; This specifies the command type DFB CMDNUM ; word pointer to the parameter list DW CMDLIST ; carry is set on an error BCS ERROR --^-- That's exactly what we're doing at $28B4 -- calling the command dispatch. The next three bytes are a command number and the address of a parameter block. Then we branch to The Badlands at $28D5 on error. 28B4- 20 99 28 JSR $2899 28B7- [04] 28B8- [76 29] 28BA- B0 19 BCS $28D5 In this first call, we're issuing the SmartPort command #$04 with a parameter block at $2976. This is a "control call" -- an extension mechanism to send commands that different devices can interpret in different ways. The control calls for the UniDisk 3.5 are documented later in chapter 7. (This, by the way, explains why the program "requires" a UniDisk 3.5 drive. It's not the program that requires it, it's the copy protection.) The parameter block at $2976 gives the details on this "control call." *2976. 2976- .. .. .. .. .. .. 03 01 2978- 7B 29 06 02 00 05 03 Taking this one byte at a time: $2976: $03 parameter count $2977: $01 unit number (possibly self- modified above to support running from drive 2) $2978: $297B address of "control list," i.e. the parameter block for this custom call $297A: $06 control code: "SetAddress" $297B: $0002 block size (always 2) $297D: $0305 address within the UniDisk drive The firmware reference manual describes the "SetAddress" control call: --v-- This call is used to set the address in the UniDisk 3.5 controller's memory space that the Download call will load a 65C02 routine into. Care must be taken that the download address is set only to free space in the UniDisk 3.5 memory map. --^-- So we're setting up an environment to transfer executable code to the drive itself. Because that's not completely insane. (Yes, I'm aware that this was common on other platforms like the Commodore 64. That doesn't make it any less insane.) So what are we downloading? That's the next call, at $28BC: 28BC- 20 99 28 JSR $2899 28BF- [04] 28C0- [7E 29] 28C2- B0 11 BCS $28D5 Same deal, we're calling the SmartPort firmware with a control call. $28BF is #$04, a control call command. $28C0 is $297E, the address of the parameter block. And we branch to The Badlands on error. Looking at the parameter block: *297E. 297E- .. .. .. .. .. .. 03 01 2980- 00 2A 07 We see a similar structure as the first call, but with a different control code (at $2982): $297E: $03 parameter count $297F: $01 unit number (possibly self- modified above to support running from drive 2) $2980: $2A00 address of parameter block $2982: $07 control code: "Download" This is what the firmware reference manual has to say about the "Download" call: --v-- This call is used to download an executable 65C02 routine into the memory resident on the UniDisk 3.5 controller. The address that the routine is loaded into is set by the SetAddress call. The count field must be set to the length of the 65C02 routine to be downloaded. --^-- So $2980 points to $2A00, which will contain a length word followed by the actual code to download to the drive. The code is 65c02 code, so I can use the monitor disassembly to read it. *2A00. 2A00- 70 00 #$70 bytes of code, starting at $2A02. But it will be executed on the drive itself, at address $0305. We'll look at it in a moment. First, for completeness, I want to note that this "Download" control call does not auto-execute the code it transfers to the drive. That happens in the third call, at 28C4: 28C4- 20 99 28 JSR $2899 28C7- [04] 28C8- [83 29] 28CA- B0 09 BCS $28D5 $28C7 is #$04, so we are once again doing a control call. $28C8 points to the parameter block at 2983. We jump to The Badlands if there's any error. *2983. 2983- .. .. .. 03 01 88 29 05 $2983: $03 parameter count $2984: $01 unit number (possibly self- modified above to support running from drive 2) $2985: $2988 address of parameter block $2987: $05 control code: "Execute" Again from the fine manual: --v-- This call is used to dispatch the intelligent controller in the UniDisk 3.5 device to execute a 65C02 subroutine. The register setup is passed to the routine to be executed from the control list. --^-- The parameter block at $2988 gives the initial values of each register. (This is a 65c02, so it has A, X, and Y registers just like the Apple II its connected to.) *2988. 2988- 06 00 05 05 34 00 05 03 $2988: $0006 block size (always 6) $298A: $05 A value $298B: $05 X value $298C: $34 Y value $298D: $00 flags value (like PHP/PLP) $298E: $0305 address of code to execute Unsurprisingly, we are executing the code we just downloaded to $0305. Now let's look at that code. ~ Chapter 4 In Which Murphy's Law Never Fails Taking advantage of the fact that this floppy drive runs the same processor as the host Apple II (I AM STILL NOT OVER THAT, BY THE WAY), we can manually move the code to $0305 on the Apple II and use the monitor disassembly to list it. *305<2A02.2A71M *305L ; save some zero page addresses 0305- A5 73 LDA $73 0307- 8D 47 05 STA $0547 030A- A5 74 LDA $74 030C- 8D 48 05 STA $0548 ; copy some zero page values 030F- A0 05 LDY #$05 0311- B1 73 LDA ($73),Y 0313- 99 20 05 STA $0520,Y 0316- 88 DEY 0317- D0 F8 BNE $0311 ; overwrite some zero page addresses 0319- A9 4C LDA #$4C 031B- 85 75 STA $75 031D- A9 20 LDA #$20 031F- 85 73 STA $73 0321- A9 05 LDA #$05 0323- 85 74 STA $74 ; call... something in ROM 0325- 20 6A E5 JSR $E56A 0328- 20 62 E1 JSR $E162 032B- EA NOP ; get a raw nibble from the disk (like ; "LDA $C08C,X" on a 5.25-inch floppy) 032C- AD 0E 0A LDA $0A0E 032F- 10 FB BPL $032C ; loop until we find a $D5 nibble 0331- C9 D5 CMP #$D5 0333- D0 F7 BNE $032C ; burn CPU cycles 0335- 48 PHA 0336- 68 PLA ; next nibble must be $B5 (nonstandard) 0337- AD 0E 0A LDA $0A0E 033A- 10 FB BPL $0337 033C- C9 B5 CMP #$B5 ; otherwise loop back to find another ; $D5 nibble 033E- D0 EC BNE $032C ; restore RdAddr hooks in zero page 0340- AD 47 05 LDA $0547 0343- 85 73 STA $73 0345- AD 48 05 LDA $0548 0348- 85 74 STA $74 034A- 38 SEC 034B- 60 RTS According to the fine manual, zero page $73 and $74 are part of a "hook table" that jumps to all the hookable routines the drive supports. This includes routines like "RdAddr: find and decode an address field" ($73/$74), "ReadData: find and load a data field in RAM" ($76/$77), and so on. So we're setting the "RdAddr" hook to point to $0520. Well, we just copied 5 bytes of code from ($73) to $0521 (at $030F), but nothing to address $0520. The fine manual lists $0500..$05FF as "free space," so I'm confused. What exactly is supposed to be at $0520? The answer is... a bit shocking. This entire copy protection routine was set up incorrectly. We're not supposed to be downloading code to the address $0305 inside the floppy drive. No, really, we're not supposed to do that. The fine manual says that's part of a buffer for "host communication (format sector buffer I)." I'm not sure what that is, but it is definitely not "free space." The $0500..$05FF page is listed listed as "free space." $0305 is not. The root cause, I believe, is an off- by-1 bug in the parameter blocks of the original control calls. This code isn't supposed to be downloaded and executed at $0305; it's supposed to be at $0500. That makes more sense on its face, just because that's the largest block of free space in the drive's memory map. But also, it would make the code itself make more sense. Observe. Suppose, for the sake of argument, that this code ended up at $0500 instead of $0305. Then it would look like this: 0500- A5 73 LDA $73 0502- 8D 4C 05 STA $054C 0505- A5 74 LDA $74 0507- 8D 4D 05 STA $054D 050A- A0 05 LDY #$05 050C- B1 73 LDA ($73),Y 050E- 99 20 05 STA $0520,Y 0511- 88 DEY 0512- D0 F8 BNE $050C 0514- A9 4C LDA #$4C 0516- 85 75 STA $75 0518- A9 20 LDA #$20 051A- 85 73 STA $73 051C- A9 05 LDA #$05 051E- 85 74 STA $74 ; now these two JSR calls are self- ; modified by the code above 0520- 20 6A E5 JSR $E56A 0523- 20 62 E1 JSR $E162 0526- EA NOP 0527- AD 0E 0A LDA $0A0E 052A- 10 FB BPL $0527 052C- C9 D5 CMP #$D5 052E- D0 F7 BNE $0527 0530- 48 PHA 0531- 68 PLA 0532- AD 0E 0A LDA $0A0E 0535- 10 FB BPL $0532 0537- C9 B5 CMP #$B5 0539- D0 EC BNE $0527 053B- AD 4C 05 LDA $054C 053E- 85 73 STA $73 0540- AD 4D 05 LDA $054D 0543- 85 74 STA $74 0545- 38 SEC 0546- 60 RTS If this had been downloaded to, and executed from, address $0500, the RdAddr hook at $73/$74 would have been redirected to $0520. The code at $520 would have been self-modified to be the first 5 bytes of code from the original RdAddr routine, followed by the custom code starting at $0527. That custom code would check for a nonstandard nibble sequence on the next disk read, looping forever if it couldn't find it. (The drive has a "watchdog" timeout, so this would eventually just time out and return an error to the host Apple II.) If it succeeded in finding the custom nibble sequence, it would have restored the RdAddr hook at $73/$74 before returning. All that, combined with the final SmartPort call at $28CC... 28CC- 20 99 28 JSR $2899 28CF- [01] 28D0- [57 29] Command #$01 is a "read" command. So we would have the drive read a block from the disk, but with the hooked RdAddr routine that points to the protection code at $0520. *2957. 2957- .. .. .. .. .. .. .. 03 2958- 01 00 02 10 00 00 $2957: $03 parameter count $2958: $01 unit number (possibly self- modified above to support running from drive 2) $2959: $0200 address of read buffer (in the drive memory) $295B: $000010 block number And *that* SmartPort call would succeed or fail based on whether it found the custom nibble sequence ($D5 $B5) while attempting to read block $10. And *that* is the entire protection: a single custom nibble near block $10. Except none of that happened, because there was an off-by-1 bug in the parameter block of the control call, so the code that self-modified as if were at $0500 ended up being at $0305 instead, and I HAVE LITERALLY NO IDEA HOW THIS EVER WORKED AT ALL, even on an original disk with a compatible UniDisk 3.5 drive. The RdAddr hook ($73/$74) ends up pointing to uninitialized memory at $0520, but because the JSRs at $0325 are never self-modified, it falls through to the actual protection code to check for a custom nibble sequence, but during the Execute call instead of the final ReadBlock call, then restores the RdAddr hook before returning. So the ReadBlock call will always succeed, because by the time it happens, the RdAddr hook has been restored to its original value. I think. I have seen variants of this protection on two different disks, and in both cases, the //e code, that downloads executable code to the drive, had this off-by-1 bug. Presumably the IIgs code (which I did not show) actually fails properly on an unauthorized copy, and that's primarily what the developers tested. At any rate, copy protection barely works in the best of circumstances, which these are not. Downloading code to peripherals is insane in the best of circumstances, which these are not. I will die on this hill. (*) (*) not guaranteed, actual death may vary The protection routine has no side effects. We can bypass the entire thing by changing the JMP at $2C6D to jump directly to the success path at $2922. Using Glen Bredon's "Block Warden," I can search the disk for the offending JSR. The syntax always confuses me, so for future me, the correct sequence is [C]hange device to "Slot 7, Drive 2" [E]dit [Ctrl-S] and enter "$6C712C" as the search string to search for a hex sequence (this is the indirect JMP at $2C6D) It finds the code on block $003A. ; instead of entering the copy ; protection routine, jump directly to ; the success path $003A,$6D: 6C 71 2C -> 4C 22 29 As an added bonus, we're removed the only thing that tied us to the UniDisk 3.5, so I can boot my cracked copy on my Apple SuperDrive. (I tried copying it to a subdirectory on my ProDOS hard drive, but it fails with a "Directory not found" error. A project for another day.) Anyway, lovely game. Shame about the protection. Quod erat liberandum. --------------------------------------- A 4am crack No. 2142 ------------------EOF------------------