--------------Quiz Castle-------------- A 4am crack 2018-08-31 --------------------------------------- Name: Quiz Castle Genre: educational Year: 1986 Credits: Jeffrey Jones Publisher: Didatech Software Platform: Apple ][+ or later Media: 5.25-inch disk Sides: 2 OS: Pronto-DOS Previous cracks: none ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways COPYA fails on first pass Locksmith Fast Disk Backup can read every sector except T02,S07; copy displays title screen then quits to BASIC prompt with DOS disconnected EDD 4 bit copy (no sync, no count) ditto Copy ][+ nibble editor There's an address field for T02,S07, but no data Disk Fixer T00 -> DOS 3.3-shaped RWTS T11 -> DOS 3.3 disk catalog T01,S07 -> startup program is "HELLO" Why didn't COPYA work? intentionally unreadable sector on track $02 Why didn't Locksmith FDB / EDD work? probably a nibble check that checks that unreadable sector Next steps: 1. find runtime protection check 2. disable it 3. declare victory (*) (*) go to the gym ~ Chapter 1 In Which It's Better To Be Lucky Than Good Since my copy goes down a different code path than the original, I'm guessing there is a runtime protection check somewhere. One thing that all protection checks have in common is they turn on the drive motor by accessing a specific address in the $C0xx range. For slot 6, it's $C0E9, but to allow disks to boot from any slot, developers usually use code like this: LDX LDA $C089,X There's nothing that says where the slot number has to be, although the disk controller ROM routine uses zero page $2B and lots of disks just reuse that. There's also nothing that says you have to use the X-register as the index, or that you must use the accumulator as the load register. But most RWTS code does, out of convention I suppose (or possibly fear of messing up such low-level code in subtle ways). Also, since developers don't actually want people finding their protection- related code, they may try to encrypt it or obfuscate it to prevent people from finding it. But eventually, the code must exist and the code must run, and it must run on my machine, and I have the final say on what my machine does or does not do. But sometimes you get lucky. Turning to my trusty Disk Fixer sector editor, I search the non-working copy for "BD 89 C0", which is the opcode sequence for "LDA $C089,X". [Disk Fixer] ["F"ind] ["H"ex] ["BD 89 C0"] --v-- ------------- DISK SEARCH ------------- $00/$07-$4F $01/$0A-$46 --^-- The first match on track 0 is part of the standard DOS 3.3 RWTS, i.e. not suspicious. On an unprotected DOS 3.3 disk, that would be the only match, so literally any other matches are suspicious -- including this one on track 1, which is doubly weird because that's smack in the middle of DOS itself. Booting the disk and pressing gets me to a working prompt with DOS in memory, so let's see it in its native environment. ]PR#6 ... ]CALL -151 Track 1, sector $0A would be loaded at $AF00. Except this is Pronto-DOS, so everything is shifted by 2 sectors. So $AD00. *AD46L AD46- BD 89 C0 LDA $C089,X There it is. ~ Chapter 2 In Which All Bits Are Equal, But Some Are More Equal Than Others From inspection, the protection routine doesn't start at $AD46; it starts at $ACEF. (Before that is an unconditional JMP instruction.) *ACEFL ; ($FE) -> $6000 ACEF- A0 00 LDY #$00 ACF1- 84 FE STY $FE ACF3- 84 06 STY $06 ACF5- A9 60 LDA #$60 ACF7- 85 FF STA $FF ; pass a byte to $ADC7 ACF9- A9 BD LDA #$BD ACFB- 20 C7 AD JSR $ADC7 *ADC7L ; ...which stores it in ($FE), which ; starts at $6000 and is incremented ; after each byte stored ADC7- 91 FE STA ($FE),Y ADC9- E6 FE INC $FE ADCB- D0 02 BNE $ADCF ADCD- E6 FF INC $FF ADCF- 60 RTS Ah! We're sneakily creating code, one byte at a time. Continuing from $ACFE... ; more sneaky code generation ACFE- A9 8C LDA #$8C AD00- 20 C7 AD JSR $ADC7 AD03- A9 C0 LDA #$C0 AD05- 20 C7 AD JSR $ADC7 AD08- A9 8D LDA #$8D AD0A- 20 C7 AD JSR $ADC7 AD0D- A9 C0 LDA #$C0 AD0F- 18 CLC AD10- 65 06 ADC $06 AD12- 20 C7 AD JSR $ADC7 AD15- A9 60 LDA #$60 AD17- 20 C7 AD JSR $ADC7 AD1A- E6 06 INC $06 AD1C- A5 06 LDA $06 AD1E- C9 1E CMP #$1E AD20- 90 D7 BCC $ACF9 ; one final byte (looks like an "RTS") AD22- A9 60 LDA #$60 AD24- 20 C7 AD JSR $ADC7 ; get address of RWTS parameter table AD27- 20 E3 03 JSR $03E3 ; ($08) -> RWTS parameter table AD2A- 84 08 STY $08 AD2C- 85 09 STA $09 ; track = $02 AD2E- A9 02 LDA #$02 AD30- A0 04 LDY #$04 AD32- 91 08 STA ($08),Y ; command = $00 (seek) AD34- A9 00 LDA #$00 AD36- A0 0C LDY #$0C AD38- 91 08 STA ($08),Y ; volume = $00 (wildcard, matches any) AD3A- A0 03 LDY #$03 AD3C- 91 08 STA ($08),Y ; execute the command, seek to track 2 AD3E- 20 E3 03 JSR $03E3 AD41- 20 D9 03 JSR $03D9 ; bail if that failed for some reason AD44- B0 27 BCS $AD6D ; and we're back to the instruction ; that led us here in the first place, ; turning on the drive motor manually AD46- BD 89 C0 LDA $C089,X ; initialize Death Counter AD49- A9 30 LDA #$30 AD4B- 8D 78 05 STA $0578 AD4E- 38 SEC AD4F- CE 78 05 DEC $0578 AD52- F0 19 BEQ $AD6D ; find next address field AD54- 20 44 B9 JSR $B944 AD57- B0 F5 BCS $AD4E ; check sector number AD59- A5 2D LDA $2D ; we want logical sector 7, which is ; physical sector 1 AD5B- C9 01 CMP #$01 AD5D- D0 EF BNE $AD4E ; reset data latch AD5F- BD 8E C0 LDA $C08E,X ; wait AD62- A9 06 LDA #$06 AD64- 20 A8 FC JSR $FCA8 ; and call our mystery subroutine that ; we generated one byte at a time AD67- 20 00 60 JSR $6000 It's time to see what's at $6000. We can run this code from the monitor as long as we stop before doing any disk stuff. ; stop after sneaky code generation and ; before doing any disk stuff *AD27:60 ; run just the code generation routine *ACEFG Now let's see what we built. *6000L 6000- BD 8C C0 LDA $C08C,X 6003- 8D C0 60 STA $60C0 6006- BD 8C C0 LDA $C08C,X 6009- 8D C1 60 STA $60C1 600C- BD 8C C0 LDA $C08C,X 600F- 8D C2 60 STA $60C2 6012- BD 8C C0 LDA $C08C,X 6015- 8D C3 60 STA $60C3 6018- BD 8C C0 LDA $C08C,X 601B- 8D C4 60 STA $60C4 601E- BD 8C C0 LDA $C08C,X 6021- 8D C5 60 STA $60C5 6024- BD 8C C0 LDA $C08C,X 6027- 8D C6 60 STA $60C6 602A- BD 8C C0 LDA $C08C,X 602D- 8D C7 60 STA $60C7 6030- BD 8C C0 LDA $C08C,X 6033- 8D C8 60 STA $60C8 6036- BD 8C C0 LDA $C08C,X 6039- 8D C9 60 STA $60C9 603C- BD 8C C0 LDA $C08C,X 603F- 8D CA 60 STA $60CA 6042- BD 8C C0 LDA $C08C,X 6045- 8D CB 60 STA $60CB 6048- BD 8C C0 LDA $C08C,X 604B- 8D CC 60 STA $60CC 604E- BD 8C C0 LDA $C08C,X 6051- 8D CD 60 STA $60CD 6054- BD 8C C0 LDA $C08C,X 6057- 8D CE 60 STA $60CE 605A- BD 8C C0 LDA $C08C,X 605D- 8D CF 60 STA $60CF 6060- BD 8C C0 LDA $C08C,X 6063- 8D D0 60 STA $60D0 6066- BD 8C C0 LDA $C08C,X 6069- 8D D1 60 STA $60D1 606C- BD 8C C0 LDA $C08C,X 606F- 8D D2 60 STA $60D2 6072- BD 8C C0 LDA $C08C,X 6075- 8D D3 60 STA $60D3 6078- BD 8C C0 LDA $C08C,X 607B- 8D D4 60 STA $60D4 607E- BD 8C C0 LDA $C08C,X 6081- 8D D5 60 STA $60D5 6084- BD 8C C0 LDA $C08C,X 6087- 8D D6 60 STA $60D6 608A- BD 8C C0 LDA $C08C,X 608D- 8D D7 60 STA $60D7 6090- BD 8C C0 LDA $C08C,X 6093- 8D D8 60 STA $60D8 6096- BD 8C C0 LDA $C08C,X 6099- 8D D9 60 STA $60D9 609C- BD 8C C0 LDA $C08C,X 609F- 8D DA 60 STA $60DA 60A2- BD 8C C0 LDA $C08C,X 60A5- 8D DB 60 STA $60DB 60A8- BD 8C C0 LDA $C08C,X 60AB- 8D DC 60 STA $60DC 60AE- BD 8C C0 LDA $C08C,X 60B1- 8D DD 60 STA $60DD 60B4- 60 RTS Normal RWTS code reads a nibble from disk in a tight loop, like this: @ LDA $C08C,X BPL @ This waits for the high bit to be set, which signifies that the entire nibble has "entered" the data latch and is complete. (All valid nibbles have the high bit set.) This code, on the other hand, has no BPL loops. This will just keep reading the data latch as fast as possible and storing the raw partial nibble values in $60C0..$60DD. So this will capture all sorts of "intermediate" values, the ones that a normal RWTS would discard because they weren't a complete nibble value yet. This is as close to a raw bitstream as you can get. You don't generally see code like this. It's not useful for reading data, because it doesn't wait for a complete nibble. And it's not practical for bit copiers, because it only captures a small section of the bitstream on the track -- 30 bits out of about 50,000. But I bet these 30 bits are really important. ~ Chapter 3 In Which Our Adventure Takes An Unexpectedly Nasty Turn Continuing from $AD6A, after returning from the raw bit reading code at $6000: ; unconditional jump AD6A- 18 CLC AD6B- 90 04 BCC $AD71 ; failure ends up here (from $AD44) -- ; get the RWTS error code AD6D- A0 0D LDY #$0D AD6F- B1 08 LDA ($08),Y ; execution continues here regardless, ; and we turn off the drive motor AD71- 9D 88 C0 STA $C088,X ; reset zero page after RWTS call AD74- A0 00 LDY #$00 AD76- 84 48 STY $48 ; branch forward on previous failure ; (success path will still have the ; carry bit clear) AD78- B0 38 BCS $ADB2 ; initialize Death Counter AD7A- 84 FE STY $FE ; get an address from a lookup table ; that's part of this protection ; routine AD7C- A2 00 LDX #$00 AD7E- 84 06 STY $06 AD80- BD D0 AD LDA $ADD0,X AD83- 85 08 STA $08 AD85- BD D5 AD LDA $ADD5,X AD88- 85 09 STA $09 The addresses at $ADD0 and $ADD5 point to different sequences at $ADDA, $ADE3, and a few others. Here's one of them: *ADDA. ADD8- .. .. 05 08 0A 0B 10 14 ADE0- 15 16 FF .. .. .. .. .. Then we scan through that sequence (each sequence is terminated with an $FF byte) and look for an exact match within the raw data latch values we captured at $6000 and stored at $60C0: AD8A- A0 00 LDY #$00 AD8C- B1 08 LDA ($08),Y AD8E- C9 FF CMP #$FF AD90- D0 0A BNE $AD9C ; went through this entire sequence ; without a match, so increment Death ; Counter and start over with the first ; match sequence again AD92- E6 FE INC $FE AD94- A4 FE LDY $FE AD96- C0 1A CPY #$1A AD98- 90 E2 BCC $AD7C ; protection check failed, branch to ; failure path AD9A- B0 16 BCS $ADB2 ; Check the partial nibble values that ; were stored by the routine at $6000. ; Each time through the loop, we ; examine a specific raw nibble value. ; So I guess we're not looking for an ; entire sequence of partial nibble ; values? Just whether the first one is ; in any of the tables of acceptable ; values, then the second, then the ; third, &c. AD9C- 84 07 STY $07 AD9E- A4 06 LDY $06 ADA0- D9 C0 60 CMP $60C0,Y ; found a match for this partial nibble ; value, so branch forward to continue ADA3- F0 05 BEQ $ADAA ; otherwise keep looking for a match ADA5- A4 07 LDY $07 ADA7- C8 INY ADA8- D0 E2 BNE $AD8C ; execution continues here regardless, ; possibly from $ADA3 or by falling ; through -- ; try the next sequence (there are 5) ADAA- E6 06 INC $06 ADAC- E8 INX ADAD- E0 05 CPX #$05 ADAF- 90 CF BCC $AD80 ; protection routine passed, we matched ; enough partial nibble values -- ; clear carry bit and continue ADB1- 18 CLC ; execution continues here regardless ; of whether the protection passed (we ; also end up here from $AD9A if the ; protection failed, but with the carry ; bit set) ADB2- A0 FE LDY #$FE ADB4- 90 01 BCC $ADB7 ; only executed if the protection check ; failed ADB6- C8 INY ADB7- 8C FF B7 STY $B7FF OK, so there's the difference between an original disk and a copy: the value of the Y register at $ADB7, which gets stored in $B7FF. original = #$FE copy = #$FF ; cover our tracks by overwriting the ; JMP instruction that sent us here, ; then continue with loading DOS ADBA- A9 80 LDA #$80 ADBC- 8D 4E 9E STA $9E4E ADBF- A9 A1 LDA #$A1 ADC1- 8D 4F 9E STA $9E4F ADC4- 4C 80 A1 JMP $A180 I didn't realize it until just now, but that explains how and when this routine is called. At the end of loading DOS, it normally jumps to $A180 to load and execute the HELLO program. Based on how the protection check exits, I went back and looked at that part of the DOS, and this disk jumps to $ACEF instead, to do all this and set what I assume is a very important side effect in $B7FF. Either way, the protection check exits via the standard $A180 DOS routine. The only difference is the value in $B7FF. That's unexpectedly nasty. I love it, Returning to my trusty sector editor, I searched the disk for "FF B7" to see where this address is accessed later. --v-- ------------- DISK SEARCH ------------- $01/$0A-$B8 $08/$0A-$DB $08/$0A-$E2 --^-- Track 1 is the protection check itself, where it sets $B7FF. Track 8 must be where it's read later. --v-- T08,S0A ----------- DISASSEMBLY MODE ---------- 00D3:AD DF BC LDA $BCDF 00D6:C9 FE CMP #$FE 00D8:D0 05 BNE $00DF 00DA:4D FF B7 EOR $B7FF <-- ! 00DD:F0 0E BEQ $00ED 00DF:A9 00 LDA #$00 00E1:8D FF B7 STA $B7FF 00E4:20 58 FC JSR $FC58 00E7:20 99 F3 JSR $F399 00EA:4C 9B FA JMP $FA9B --^-- Aha! The carry is set, so Y is incremented, so $B7FF ends up with #$FF instead of #$FE, so MUCH MUCH LATER this EOR doesn't produce zero, so we fall into The Badlands (at offset $00DF) that clears the screen and exits to a BASIC prompt. Which is exactly the behavior I saw on my non-working copy. ~ Chapter 4 One Byte To Rule Them All, And In The Darkness Bind Them To make my non-working copy work like the original disk, I can change a single instruction within the protection check: ADB6- C8 INY into ADB6- EA NOP So even when the protection fails, the value at $B7FF will be correct. T01,S0A,$B6: C8 -> EA ]PR#6 ...works... Side B has identical protection. Quod erat liberandum. --------------------------------------- A 4am crack No. 2091 ------------------EOF------------------