-----Sailing Through Story Problems---- A 4am crack 2015-09-16 -------------------. updated 2015-09-16 |___________________ Name: Sailing Through Story Problems Genre: educational Year: 1987 Authors: NeoSoft Publisher: Developmental Learning Materials (DLM) Media: two 5.25-inch floppies OS: Pronto-DOS Previous cracks: none Similar cracks: Spanish Grammar Review: Future and Conditional Tenses of Regular and Irregular Verbs (crack no. 432) Disk 1 is protected but bootable. Disk 2 is unprotected but unbootable. Life is like that. This has not been a haiku. ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways COPYA disk read error on first pass Locksmith Fast Disk Backup copies everything except track $03; disk boots DOS, clears screen, then prints "DISK READ ERROR" and halts EDD 4 bit copy (no sync, no count) no errors, but copy exhibits same behavior as failed Locksmith FDB copy Copy ][+ nibble editor track $03 appears to contain data, but it has no sector headers or structure Disk Fixer T00 -> DOS 3.3 bootloader / RWTS T00-T02 -> full copy of DOS 3.3 T11 -> DOS 3.3 catalog T01,S07 -> startup program is "HELLO" Can't find any way to read track $03 Why didn't COPYA work? track $03 is intentionally damaged Why didn't Locksmith FDB / EDD work? Probably a nibble check in the HELLO program that reads track 3 during boot just reboots. A well-timed gets me a prompt, but any command reboots again. Next steps: 1. Trace the startup program 2. Disable the nibble check 3. There is no step 3 (I hope) ~ Chapter 1 In Which Things Start Off Poorly And Go Downhill Rapidly Booting from my work disk, the non- working copy ought to have a catalog, but it has suspiciously vanished. [S6,D1=non-working copy from Locksmith] [S5,D1=my work disk] ]PR#5 ... ]CATALOG,S6,D1 C1983 DSR^C#254 022 FREE Turning to my trusty Disk Fixer sector editor, I immediately go to T11,S00 to see if there is a simple fix. And there is... --v-- -------------- DISK EDIT --------------- TRACK $11/SECTOR $00/VOLUME $FE/BYTE$00 --------------------------------------- $00: 00 00 0F 03 00 00 FE 00 @@OC@@~@ ^^ track $00? I don't think so $08: 00 00 00 00 00 00 00 00 @@@@@@@@ $10: 00 00 00 00 00 00 00 00 @@@@@@@@ $18: 00 00 00 00 00 00 00 00 @@@@@@@@ $20: 00 00 00 00 00 00 00 7A @@@@@@@: $28: 00 00 00 00 00 00 00 00 @@@@@@@@ $30: 02 FF 00 00 23 10 00 01 B.@@#P@A $38: 00 00 00 00 00 00 00 00 @@@@@@@@ $40: 00 02 00 00 00 00 00 00 @B@@@@@@ $48: 00 00 00 00 00 00 00 00 @@@@@@@@ $50: 00 00 00 00 00 00 00 00 @@@@@@@@ $58: 00 00 00 00 00 00 00 00 @@@@@@@@ $60: 00 00 00 00 00 00 00 00 @@@@@@@@ $68: 00 00 00 00 00 00 00 00 @@@@@@@@ $70: 00 00 00 00 00 00 00 00 @@@@@@@@ $78: 00 00 00 00 00 00 00 00 @@@@@@@@ --------------------------------------- BUFFER 0/SLOT 6/DRIVE 1/MASK OFF/NORMAL --------------------------------------- COMMAND : _ --^-- The DOS on this disk apparently hard-codes the track number, and the disk catalog on track $11 has a bogus track number. (Further inspection of track $11 confirms that there really is a standard disk catalog on the disk. The only problem is that third-party disks can't see it because they trust T11,S00 to tell them where to look first.) But how does the original disk know where to look? I scoured "Beneath Apple DOS" until I found the answer on p8-28: --v-- B011-B036 Read a directory sector ; (If CARRY flag is zero on entry, read first directory sector. If CARRY is one, read next) ; Memorize entry code. ; Set buffer pointers (B045). ; First or next? ; If first, get track/sector of directory sector from VTOC at offset +1,+2. ; Otherwise, get track/sector from directory sector at offset +1,+2. If track is zero, exit with error code (end of directory). ; Call RWTS to read sector. ; Exit with normal return code. --^-- So, to read the first sector of file names and other metadata, this routine is supposed to look at the VTOC sector buffer (read from T11,S00 and stored at $B3BB..$B4BA). The VTOC says "hey, the first sector of files and stuff is in T11,S0F" so this routine is supposed to read T11,S0F. But the DOS on this disk made one small modification to that routine. (This is on T01,S0F.) B011- 08 PHP B012- 20 45 B0 JSR $B045 B015- 28 PLP B016- B0 08 BCS $B020 B018- AC BD B3 LDY $B3BD ------ B01B- A2 11 LDX #$11 << hey B01D- EA NOP << now ------ B01E- D0 0A BNE $B02A B020- AE BC B4 LDX $B4BC B023- D0 02 BNE $B027 B025- 38 SEC B026- 60 RTS B027- AC BD B4 LDY $B4BD B02A- 8E 97 B3 STX $B397 B02D- 8C 98 B3 STY $B398 B030- A9 01 LDA #$01 B032- 20 52 B0 JSR $B052 B035- 18 CLC B036- 60 RTS Instead of getting the track number from the VTOC, it hard-codes track $11. Now that I've identified the problem, the fix is straightforward. If I change the VTOC header (T11,S00) to point to the actual first directory sector (T11,S0F), DOS 3.3 or any other copy utility should be able to read the disk catalog. T11,S00,$01 change 00 to 11 ]PR#5 ... ]CATALOG,S6,D1 ]C C1983 DSR^C#254 003 FREE B 089 DLMPIRATE1 *A 027 G1 *A 022 G10 *A 032 G2 *A 025 G3 *A 024 G4 *A 024 G5 *A 026 G6 *A 024 G7 *A 024 G8 *A 021 G9 *A 031 M *A 024 SCORE *B 023 SS0 *B 014 SS1 *B 009 SS2 *B 005 SS3 *B 024 SS4 *B 022 SS5 B 002 SSPROT$$A A 002 HELLO B 005 SSPROT$$1 That's better. ~ Chapter 2 But Wait, It Gets Worse ]LOAD HELLO,S6,D1 ]LIST 5 POKE 254,128 + ASC ("A") 10 PRINT "BRUN SSPROT$$1" 20 END ]BLOAD SSPROT$$1 ]CALL -151 ; Diversi-DOS 64K stores the last BLOAD ; address here *BF55.BF56 BF55- 00 08 *800L 0800- D8 CLD 0801- 20 0B 08 JSR $080B 0804- 4C 00 09 JMP $0900 *80BL ; pop return address and put it in two ; seemingly arbitrary locations 080B- 68 PLA 080C- 8D 01 0B STA $0B01 ; =$03 080F- 68 PLA 0810- 8D 56 08 STA $0856 ; =$08 ; compute a checksum to make sure ; nothing has been modified 0813- A9 00 LDA #$00 0815- AA TAX 0816- 5D 00 08 EOR $0800,X 0819- CA DEX 081A- F0 03 BEQ $081F 081C- 4C 16 08 JMP $0816 ; store the checksum in what was ; executable code just a few cycles ago 081F- 8D 0F 08 STA $080F ; set the BRK vector to reboot 0822- A9 00 LDA #$00 0824- 8D F0 03 STA $03F0 0827- A9 C6 LDA #$C6 0829- 8D F1 03 STA $03F1 ; and now a decryption loop that ; decrypts based on the checksum of the ; code 082C- A9 08 LDA #$08 082E- 85 B0 STA $B0 0830- A0 53 LDY #$53 0832- A9 00 LDA #$00 0834- 85 AF STA $AF 0836- 85 FF STA $FF 0838- A5 FF LDA $FF 083A- 51 AF EOR ($AF),Y 083C- 4D 0F 08 EOR $080F 083F- 91 AF STA ($AF),Y 0841- 45 FF EOR $FF 0843- 85 FF STA $FF 0845- EE 0F 08 INC $080F 0848- C8 INY 0849- D0 ED BNE $0838 084B- E6 B0 INC $B0 084D- A5 B0 LDA $B0 084F- C9 0A CMP #$0A 0851- D0 E5 BNE $0838 ; everything after this is encrypted 0853- 79 2D 24 ADC $242D,Y 0856- 08 PHP 0857- A0 AB LDY #$AB 0859- 0A ASL 085A- 79 58 82 ADC $8258,Y 085D- 80 ??? 085E- 56 57 LSR $57,X 0860- D9 2A 2E CMP $2E2A,Y ; note that this byte in particular was ; modified earlier based on the return ; address on the stack 0856- 08 PHP One thing at a time. This routine at $080B is called from $0801, so the top of the stack is going to contain $03, then $08. $03 goes into $0B01 and $08 goes into $0856. Given that, I can reproduce the checksum calculation elsewhere to determine the decryption key that ends up in $080F. *B01:03 ; originally from stack *856:08 ; originally from stack *8000<800.8FFM ; copy everything *801E:80 ; fix in-loop JMP *8022:60 ; stop after checksum *8013G ; calculate checksum *80F ; and the answer is... 080F- 35 *8053:60 ; stop after decryption *802CG ; decrypt *853L 0853- 4C 57 08 JMP $0857 Well, that's something. ~ Chapter 3 In Which We Get The Distinct Impression That We're Swimming Against The Current *857L ; execute an RWTS command with a ; custom parameter table located at ; $0874 0857- A9 08 LDA #$08 0859- A0 74 LDY #$74 085B- 20 D9 03 JSR $03D9 *874.888 0874- 01 60 01 00 0878- 03 00 85 08 00 09 00 00 ^^ track $03 0880- 00 00 00 60 01 00 01 EF 0888- D8 Aha, we're seeking to the unreadable track ($03). Probably setting up for a nibble check on the raw data there. ; set reset vector 085E- A9 00 LDA #$00 0860- 8D F2 03 STA $03F2 0863- A9 C6 LDA #$C6 0865- 8D F3 03 STA $03F3 0868- A9 63 LDA #$63 086A- 8D F4 03 STA $03F4 ; set RUN flag (makes every command ; typed from the BASIC prompt execute ; RUN instead) 086D- A9 80 LDA #$80 086F- 85 D6 STA $D6 0871- 4C 89 08 JMP $0889 *889L ; turn on drive motor (hard-coded to ; slot 6) 0889- A2 60 LDX #$60 088B- BD 8E C0 LDA $C08E,X 088E- BD 8C C0 LDA $C08C,X 0891- BD 8A C0 LDA $C08A,X 0894- BD 89 C0 LDA $C089,X A fun(*) thing to do is boot original floppies from slot 5. Lots of copy protection routines (including this one) hard-code slot 6, so you can find out when they're called because the slot 6 drive light will suddenly go on. (*) not guaranteed, actual fun may vary ; wait loop 0897- A9 AA LDA #$AA 0899- 20 A8 FC JSR $FCA8 089C- A9 40 LDA #$40 089E- 85 FA STA $FA ; skip some nibbles 08A0- BD 8C C0 LDA $C08C,X 08A3- 10 FB BPL $08A0 08A5- C6 F9 DEC $F9 08A7- D0 F7 BNE $08A0 08A9- C6 FA DEC $FA 08AB- D0 F3 BNE $08A0 08AD- AD AC 09 LDA $09AC 08B0- 8D AD 09 STA $09AD 08B3- 20 45 09 JSR $0945 *945L ; initialize state 0945- A0 00 LDY #$00 0947- 18 CLC 0948- 2A ROL 0949- 85 FB STA $FB ; look for a specific nibble sequence ; "F7 F6 EF EE" while keeping some sort ; of rolling checksum 094B- AD EC C0 LDA $C0EC 094E- 10 FB BPL $094B 0950- AA TAX 0951- 45 FB EOR $FB 0953- 2A ROL 0954- 49 41 EOR #$41 0956- 85 FB STA $FB ; if Death Counter rolls over to $00 ; (meaning we couldn't find this nibble ; sequence), branch to the failure path ; which turns off the drive motor and ; displays "DISK READ ERROR" message 0958- C8 INY 0959- F0 B3 BEQ $090E 095B- 8A TXA 095C- C9 F7 CMP #$F7 095E- D0 EB BNE $094B 0960- AD EC C0 LDA $C0EC 0963- 10 FB BPL $0960 0965- AA TAX 0966- 45 FB EOR $FB 0968- 2A ROL 0969- 49 41 EOR #$41 096B- 85 FB STA $FB 096D- C8 INY 096E- 8A TXA 096F- C9 F7 CMP #$F7 0971- F0 ED BEQ $0960 0973- C9 F6 CMP #$F6 0975- D0 D4 BNE $094B 0977- AD EC C0 LDA $C0EC 097A- 10 FB BPL $0977 097C- AA TAX 097D- 45 FB EOR $FB 097F- 2A ROL 0980- 49 41 EOR #$41 0982- 85 FB STA $FB 0984- C8 INY 0985- 8A TXA 0986- C9 F7 CMP #$F7 0988- F0 D6 BEQ $0960 098A- C9 EF CMP #$EF 098C- D0 BD BNE $094B 098E- AD EC C0 LDA $C0EC 0991- 10 FB BPL $098E 0993- AA TAX 0994- 45 FB EOR $FB 0996- 2A ROL 0997- 49 41 EOR #$41 0999- 85 FB STA $FB 099B- C8 INY 099C- 8A TXA 099D- C9 F7 CMP #$F7 099F- F0 BF BEQ $0960 09A1- C9 EE CMP #$EE 09A3- D0 A6 BNE $094B 09A5- AD EC C0 LDA $C0EC 09A8- 10 FB BPL $09A5 09AA- 60 RTS Continuing from $08B6... ; compare the nibble we just read ; (at $09A5) 08B6- C9 AB CMP #$AB ; if it's not $AB, jump to failure path 08B8- D0 54 BNE $090E 08BA- A0 05 LDY #$05 08BC- 20 2D 09 JSR $092D *92DL ; match next nibbles against an array ; of $40 known nibbles (starting at ; $09AF + an initial offset given in Y) ; over and over until we find a nibble ; that doesn't match 092D- A2 00 LDX #$00 092F- AD EC C0 LDA $C0EC 0932- 10 FB BPL $092F 0934- D9 AF 09 CMP $09AF,Y 0937- D0 0A BNE $0943 0939- C8 INY 093A- 98 TYA 093B- 29 3F AND #$3F 093D- A8 TAY 093E- D0 EF BNE $092F ; count the number of times we "wrapped ; around" to the start of the array 0940- E8 INX 0941- D0 EC BNE $092F ; the number of times we wrapped around ; is the number of groups of these $40 ; nibbles that we found on the track 0943- 86 F9 STX $F9 This is the nibble array (which is what I saw on track $03 in the nibble editor earlier -- over and over): *9AF.9EE 09AF- F7 09B0- F6 EF EE AB AB AB FF D5 09B8- D5 D5 FF EA EA EA FF F5 09C0- F5 F5 FF FA FA FA FF FD 09C8- FD FD FF FE FE FE FF FF 09D0- B6 FD B7 FF BA FA BB F7 09D8- BD F7 BE BF FB BE F6 FF 09E0- FE BF BE BB EA EA FB AA 09E8- BA EA EB EF DF BF FF Then execution continues at $0945, which is the same entry point we called once before, looking for "F7 F6 EF EE". So, continuing from $08BF, after having found "F7 F6 EF EE" for a second time: ; store Y (was used to count the total ; number of nibbles it took to find the ; sequence "F7 F6 EF EE") 08BF- 84 FA STY $FA ; check the last nibble read (at $09A5) 08C1- C9 AB CMP #$AB ; wrong nibble --> off to The Badlands 08C3- D0 49 BNE $090E ; save a copy of the rolling checksum 08C5- A5 FB LDA $FB 08C7- 85 FC STA $FC ; now start over 08C9- A2 00 LDX #$00 08CB- A0 05 LDY #$05 08CD- AD EC C0 LDA $C0EC 08D0- 10 FB BPL $08CD ; go match nibbles and count nibble ; groups again (against the array at ; $09A5, same as last time) 08D2- 20 34 09 JSR $0934 ; compare the rolling checksum we got ; this time to the one we got last time ; (but don't actually care if they ; differ -- possibly some options have ; been NOP'd out of a more generalized ; protection routine?) 08D5- A5 FB LDA $FB 08D7- C5 FC CMP $FC 08D9- EA NOP 08DA- EA NOP ; get number of nibble groups we found ; during $092F..0942 (stored in $F9 at ; $0943) 08DB- A5 F9 LDA $F9 08DD- EA NOP 08DE- EA NOP ; These were initialized to be equal ; (at $08B0). $09AC is never changed, ; so this will check whether this is ; the first time we got to this point. 08DF- AE AD 09 LDX $09AD 08E2- EC AC 09 CPX $09AC 08E5- D0 08 BNE $08EF ; yes, first run -- store the number of ; nibble groups we found 08E7- 8D AE 09 STA $09AE ; loop back to do everything again ; several times 08EA- CE AD 09 DEC $09AD 08ED- D0 C4 BNE $08B3 ; execution always continues here on ; anything but the first run -- ; check that the total number of ; nibble groups we found is the same as ; it was last time 08EF- 4D AE 09 EOR $09AE ; if we found a different number of ; nibble groups this time around, jump ; to The Badlands 08F2- D0 1A BNE $090E ; loop back to do everything again ; several times 08F4- CE AD 09 DEC $09AD 08F7- D0 BA BNE $08B3 ; turn off drive 08F9- AD E8 C0 LDA $C0E8 ; check the number of nibble groups we ; found each time 08FC- AD AE 09 LDA $09AE ; needs to be at least $5C 08FF- C9 5C CMP #$5C ; otherwise -- you guessed it -- it's ; off to The Badlands 0901- 90 0B BCC $090E ; Success path falls through to here. I ; did not find any prior references to ; $0B00, so I believe it would be safe ; to jump straight to here to bypass ; the nibble check. 0903- A9 60 LDA #$60 0905- 4D 00 0B EOR $0B00 0908- 8D CF 03 STA $03CF 090B- 4C FF 09 JMP $09FF ; failure path is here -- try a few ; times before giving up completely 090E- CE AB 09 DEC $09AB 0911- D0 9A BNE $08AD ; (giving up now) turn off drive motor, 0913- AD E8 C0 LDA $C0E8 ; ...clear the screen, 0916- 20 58 FC JSR $FC58 ; ...print an error message, 0919- AD EF 09 LDA $09EF 091C- 29 7F AND #$7F 091E- AA TAX 091F- A0 00 LDY #$00 0921- B9 F0 09 LDA $09F0,Y 0924- 20 ED FD JSR $FDED 0927- C8 INY 0928- CA DEX 0929- D0 F6 BNE $0921 ; ...and hang 092B- F0 FE BEQ $092B *FC58G N 400<9F0.9FEM DISK READ ERROR ...which is exactly the behavior I saw on my non-working copy. ~ Chapter 4 Just Keep Swimming, Just Keep Swimming Now to continue (manually) on the success path at $0903. Whatever needs to end up in $03CF is probably important later, so let's pause right after that. *90B:60 *903G *9FFL 09FF- A2 00 LDX #$00 0A01- BD 0F 0A LDA $0A0F,X 0A04- 9D 00 03 STA $0300,X 0A07- E8 INX 0A08- E0 79 CPX #$79 0A0A- D0 F5 BNE $0A01 0A0C- 4C 00 03 JMP $0300 *A0C:60 ; pause again *9FFG *300L ; take the value that was POKEd by the ; HELLO program, back in the beginning 0300- A5 FE LDA $FE ; and store it later in this code 0302- 8D 76 03 STA $0376 ; don't know what this does yet 0305- A2 68 LDX #$68 0307- 20 5C 03 JSR $035C *35CL ; ah, it's executing a DOS command by ; printing a Ctrl-D followed by a ; string 035C- A9 84 LDA #$84 035E- 20 ED FD JSR $FDED 0361- E8 INX 0362- BD FF 02 LDA $02FF,X 0365- D0 F7 BNE $035E 0367- 60 RTS *FC58G N 400<368.378M BLOAD SSPROT$$AM@ The last two characters are a carriage return and a null. (They're displayed in inverse on a real machine; sorry that doesn't translate well to text.) The character before that was set at $0302 from the value POKEd by the HELLO program. So many layers. This loads another file into memory by a standard BLOAD command, so I'll reproduce that. *BLOAD SSPROT$$A Then it just returns to the caller, so let's continue the listing from there. ; oh look, another decryption loop 030A- A9 08 LDA #$08 030C- 85 68 STA $68 030E- A9 01 LDA #$01 0310- 85 67 STA $67 0312- AD FF 07 LDA $07FF 0315- 85 B0 STA $B0 0317- AC FE 07 LDY $07FE 031A- A9 00 LDA #$00 031C- 85 AF STA $AF 031E- 85 FF STA $FF 0320- A5 FF LDA $FF 0322- 51 AF EOR ($AF),Y 0324- 4D CF 03 EOR $03CF 0327- 91 AF STA ($AF),Y 0329- 45 FF EOR $FF 032B- 85 FF STA $FF 032D- EE CF 03 INC $03CF 0330- 88 DEY 0331- C0 FF CPY #$FF 0333- D0 EB BNE $0320 0335- C6 B0 DEC $B0 0337- A5 B0 LDA $B0 0339- C9 07 CMP #$07 033B- D0 E3 BNE $0320 ; now setting up a bunch of... ; Applesoft BASIC zero page globals??? 033D- AD FE 07 LDA $07FE 0340- 85 69 STA $69 0342- 85 6B STA $6B 0344- 85 6D STA $6D 0346- 85 AF STA $AF 0348- AD FF 07 LDA $07FF 034B- 85 6A STA $6A 034D- 85 6C STA $6C 034F- 85 6E STA $6E 0351- 85 B0 STA $B0 ; setting up the indirect JMP from the ; warm-start vector at $03D0 0353- AD D2 03 LDA $03D2 0356- 8D 5B 03 STA $035B ; and running the BASIC program in ; memory 0359- 6C 58 9D JMP ($9D58) Wait, what BASIC program in memory? The one we just decrypted, of course. The BLOAD SSPROT$$A command loaded an encrypted BASIC program, then decrypted it in place. Then this runs it, the hard way. It looks like the decryption loop ends at $033D, so let's pause there and see what's what. *33D:60 *30AG *3D0G ]LIST 10 PRINT CHR$ (4)"BRUN DLMPIRA TE1" Well, would you look at that. It's a perfectly normal BASIC program. ~ Chapter 5 And One More Thing... ]CALL -151 *33D:AD ; restore original code *33DL 033D- AD FE 07 LDA $07FE 0340- 85 69 STA $69 0342- 85 6B STA $6B 0344- 85 6D STA $6D 0346- 85 AF STA $AF 0348- AD FF 07 LDA $07FF 034B- 85 6A STA $6A 034D- 85 6C STA $6C 034F- 85 6E STA $6E 0351- 85 B0 STA $B0 ; uh oh 0353- AD D2 03 LDA $03D2 0356- 8D 5B 03 STA $035B 0359- 6C 58 9D JMP ($9D58) OK, one small problem -- this crashes if I've booted from my work disk, because my work disk uses Diversi-DOS 64K, so the warm-start vector at $3D0 doesn't actually point to anything in the $9Dxx range. It points to an address somewhere in $BFxx instead, but $BF58 is most decidedly *not* the entry point to run a BASIC program. I'm guessing this last bit of self- modifying code at $0353 is meant to compensate for very old computers with less than 64K of RAM. Since the rest of the DOS on the original disk assumes it loads DOS at $9D00..$BFFF, I think it's safe to ignore this and assume the vector at $9D58 is always going to be at $9D58. *356:2C ; change STA to BIT *33DG ...loads and runs without complaint... [...redo everything up to actually running the program at $033D...] *3D0G Now to move the old HELLO program out of the way and save the newly decrypted HELLO program in its place. ]RENAME HELLO,OLD HELLO ]SAVE HELLO ]PR#6 ...works... The program appears to work without setting the Applesoft zero page values, so I'm just going to leave it like this without recreating anything from the encrypted startup program. Quod erat liberandum. ~ Changelog 2015-09-16 - typos [thanks yesterbits] 2015-09-16 - initial release --------------------------------------- A 4am crack No. 450 ------------------EOF------------------