--------------Wordspinner-------------- A 4am crack 2014-04-14 --------------------------------------- Wordspinner is a 1984 educational game programmed by Dale Disharoon and distributed by The Learning Company. COPYA fails with a disk read error immediately. Locksmith Fast Disk Backup also fails spectacularly. EDD 4 bit copy gives no errors for the first half of the disk (or so), then read errors on the rest. The copy produced by EDD 4 does boot, load a bit, then hangs with the disk motor still on. The Copy ][+ nibble editor reveals that the disk uses a 6-2 encoding scheme with standard address and data headers. However, several tracks have an invalid address field that labels them as "track 0, volume 0," despite, you know, not being either of those things. Disk volume 0 is invalid, the track number is just wrong, and to top it off, the address field checksum is wrong. Here's an example (from track 1): COPY ][ PLUS BIT COPY PROGRAM 8.4 (C) 1982-9 CENTRAL POINT SOFTWARE, INC. --------------------------------------- TRACK: 01 START: 3000 LENGTH: 015F 2FE0: FF FF FF FF FF FF FF FF VIEW 2FE8: FF FF FF FF FF FF FF FF 2FF0: FF FF FF FF FF FF FF FF 2FF8: FF FF FF FF FF FF FF FF 3000: D5 AA 96 AA AA AA AA AA <-3000 ^^^^^^^^ ^^^^^^^^^^^^^^ normal invalid 3008: AA AA AA FF FF FF FF 9F ^^^^^^^^ ^^^^^^^^^^^ invalid normal 3010: E7 F9 FE FF D5 AA AD ED ^^^^^^^^ normal 3018: B4 AE 96 E9 96 9B 96 B3 3020: AE B4 B7 ED B4 ED DB D6 The invalid address field means that no sane RWTS will read this track. Which means that this RWTS, by definition, is insane. I've seen this technique before, when I cracked Ten Little Robots. That disk had invalid address fields on track 1 and 2, but they just stored a lightly modified DOS 3.3. The rest of the disk just needed to be demuffin'd; I put a fresh DOS on tracks 0-2 and called it a day. But Wordspinner has no VTOC on track $11, so I don't think this will be quite as easy. On the plus side, the game loads once and never accesses the disk once it displays the title screen. It may be possible to capture the entire thing and save it as a single file. [S6D1=original Wordspinner disk] [S5D1=my work disk] ]PR#5 ... CAPTURING BOOT0 ... SAVING BOOT0 Let's see what we have in T00,S00: ; store (slot number x 16) + $C0 ; for an unfriendly reset vector 0801- 8A TXA 0802- 4A LSR 0803- 4A LSR 0804- 4A LSR 0805- 4A LSR 0806- 09 C0 ORA #$C0 0808- 85 3F STA $3F 080A- 8D F3 03 STA $03F3 080D- 49 A5 EOR #$A5 080F- 8D F4 03 STA $03F4 0812- A9 00 LDA #$00 0814- 8D F2 03 STA $03F2 ; manually pushing a random byte to the ; stack: suspicious 0817- A9 04 LDA #$04 0819- 48 PHA 081A- 8D 81 C0 STA $C081 081D- 20 2F FB JSR $FB2F 0820- 8D 52 C0 STA $C052 0823- 20 89 FE JSR $FE89 0826- 20 93 FE JSR $FE93 ; clear hi-res screen and display it 0829- A2 20 LDX #$20 082B- A0 00 LDY #$00 082D- 84 06 STY $06 082F- A9 20 LDA #$20 0831- 85 07 STA $07 0833- 98 TYA 0834- 91 06 STA ($06),Y 0836- C8 INY 0837- D0 FB BNE $0834 0839- E6 07 INC $07 083B- CA DEX 083C- D0 F6 BNE $0834 083E- 8D 57 C0 STA $C057 0841- 8D 50 C0 STA $C050 0844- 8D 54 C0 STA $C054 0847- 8D 52 C0 STA $C052 ; Ah, it's setting up to re-use the ; disk controller ROM routine to read ; the rest of the track into $0800 ; and onward 084A- A9 5C LDA #$5C 084C- 85 3E STA $3E 084E- A9 60 LDA #$60 0850- 8D 01 08 STA $0801 ; another push, highly suspicious 0853- A9 72 LDA #$72 0855- 48 PHA ; relocate to $0200 and jump there 0856- A0 00 LDY #$00 0858- 84 FC STY $FC 085A- C8 INY 085B- A0 00 LDY #$00 085D- B9 00 08 LDA $0800,Y 0860- 99 00 02 STA $0200,Y 0863- 88 DEY 0864- D0 F7 BNE $085D 0866- 4C 69 02 JMP $0269 ; read the rest of track 0 (well, most ; of it anyway) 0269- A9 08 LDA #$08 026B- A0 01 LDY #$01 026D- A2 0E LDX #$0E 026F- 20 7A 02 JSR $027A ; don't know what this does yet ; (edit from the future: it just checks ; whether Applesoft is available on the ; machine and bails if it isn't) 0272- 20 00 15 JSR $1500 ; read one more sector into $0400 ; (this must be why we cleared the ; hi-res screen and switched to it ; so early -- because we're putting ; code in the text page) 0275- A9 04 LDA #$04 0277- AA TAX 0278- A0 0F LDY #$0F 027A- 85 27 STA $27 027C- E8 INX 027D- 86 49 STX $49 027F- 84 F9 STY $F9 0281- B9 98 02 LDA $0298,Y 0284- 85 3D STA $3D 0286- 20 93 02 JSR $0293 0289- A4 F9 LDY $F9 028B- C8 INY 028C- C4 49 CPY $49 028E- 90 EF BCC $027F 0290- A5 27 LDA $27 0292- 60 RTS 0293- A6 2B LDX $2B 0295- 6C 3E 00 JMP ($003E) Because of the two values that were manually pushed onto the stack ($04 and $72), the RTS at $0292 will "return" to $0473. That means I can let all of this code run, then interrupt the boot by changing the values that are pushed to the stack. Instead of continuing to boot, it will "continue" to a routine under my control. ; set up callback by changing values ; that are pushed to the stack. 96F8- A9 97 LDA #$97 96FA- 8D 18 08 STA $0818 96FD- A9 04 LDA #$04 96FF- 8D 54 08 STA $0854 ; start boot 9702- 4C 01 08 JMP $0801 ; callback is here ; capture $0400..$04FF at $2400 9705- A0 00 LDY #$00 9707- B9 00 04 LDA $0400,Y 970A- 99 00 24 STA $2400,Y 970D- C8 INY 970E- D0 F7 BNE $9707 ; capture $0800..$15FF to $2800 9710- A2 0E LDX #$0E 9712- B9 00 08 LDA $0800,Y 9715- 99 00 28 STA $2800,Y 9718- C8 INY 9719- D0 F7 BNE $9712 971B- EE 14 97 INC $9714 971E- EE 17 97 INC $9717 9721- CA DEX 9722- D0 EE BNE $9712 ; turn off drive 1 disk motor 9724- AD E8 C0 LDA $C0E8 ; reboot to my work disk 9727- 4C 00 C5 JMP $C500 ]BSAVE WORDSPINNER 0400-04FF, A$2400,L$100 ]BSAVE WORDSPINNER 0800-15FF, A$2800,L$E00 Execution continues at $0473, but I want to briefly describe the "insane" RWTS, which is stored at $0400..$0472. ; load X with the current slot number ; (x16) and jump to ($3E), which was ; set to point to $Cx5C way back in ; T00,S00. Furthermore, $0801 was set to an RTS, so now we can JSR $0400 and it will end up returning control after reading a sector. 0400- A6 2B LDX $2B 0402- 6C 3E 00 JMP ($003E) ; multiple calls to $040E, which ; appears to a main entry point of some ; sort. Note that the last one "falls ; through" to $040E (no RTS). So this ; is a cheap way of doing something ; multiple times. Calling $040E ; directly would do it once; calling ; $040B would do it twice; calling ; $0408 would do it three times; ; calling $0405 would do it 4 ; times. Nice. And it doesn't require ; an index register or any branching ; logic. 0405- 20 0E 04 JSR $040E 0408- 20 0E 04 JSR $040E 040B- 20 0E 04 JSR $040E 040E- 20 33 04 JSR $0433 I'll come back to the code at $0411 in a minute, but I want to skip to $0433 to follow the execution path. ; This "falls through" to $0436, so ; whatever $0436 is doing, this code ; will do it twice. I quite like this ; pattern, and apparently the original ; author did too. 0433- 20 36 04 JSR $0436 0436- 48 PHA 0437- 98 TYA 0438- 48 PHA ; load zero page $FC and manipulate it ; to... do what exactly? ORA with $2B? ; That's the current slot number (x16). 0439- A5 FC LDA $FC 043B- 85 FD STA $FD 043D- E6 FC INC $FC 043F- A5 FC LDA $FC 0441- 29 03 AND #$03 0443- 0A ASL 0444- 05 2B ORA $2B 0446- A8 TAY ; WTF is this? 0447- B9 81 C0 LDA $C081,Y 044A- A9 30 LDA #$30 044C- 20 A8 FC JSR $FCA8 044F- A5 FD LDA $FD 0451- 29 03 AND #$03 0453- 0A ASL 0454- 05 2B ORA $2B 0456- A8 TAY 0457- B9 80 C0 LDA $C080,Y 045A- A9 30 LDA #$30 045C- 20 A8 FC JSR $FCA8 045F- 68 PLA 0460- A8 TAY 0461- 68 PLA 0462- 60 RTS 0463- 00 03 05 07 09 0B 0D 0F 02 04 06 08 0A 0C When I was cracked Ten Little Robots, I found an archive of a single Usenet post from 1990 that explained how the stepper motors actually work. It is worth repeating here in its entirety. macgui.com/usenet/?group=1&id=31160 [begin quote] Basically, each track (and half-track) may be considered to be "under" one of the four phases of the stepper motor. Track Phase ---- ----- 0 0 0.5 1 1 2 1.5 3 2 0 2.5 1 3 2 3.5 3 etc. To figure the phase for a given (half-)track, multiply the track number by 2, and keep only the two low-order bits. Stepping from one track to another is simply a matter of stepping one track at a time from the original track to the destination track. Thus, to step inward from track A to track B, first step to (half-)track A+0.5, then to (half-)track A+1, and so on, until you arrive at track B. Likewise, to step outward from track B to track A, first step to (half-)track B-0.5, then to B-1, and so on until you arrive at track A. An individual step (which must from the original half-track to one if its immediately neighboring half-tracks) is accomplished by turning on the appropriate phase, waiting, and turning off the phase. An appropriate wait may be obtained by loading the accumulator with #$56 and doing a JSR to the Monitor's WAIT routine ($FCA8). (DOS and ProDOS are able to obtain improved speed by taking into account the fact that once the head is moving, it takes less time to make subsequent steps.) Note that this scheme requires DOS to keep track of which track it's on--there's no way to ask the drive where the head is. If the current track number is unknown, the head must be "recalibrated" by assuming that we're currently at track 35 (or beyond), and then seeking to track 0 (this is what causes that awful GRRRRRINDing sound when you boot a 5.25" disk). [end quote] So, to seek from the current track to the next half track, you need to 1. Set up the Y register to be a slot number (x16) plus the appropriate phase (0-3, depending on which track the drive head is on) 2. LDA $C081,Y to turn on the appropriate stepper motor 3. Wait exactly the right amount of time (as measured in CPU cycles) 4. LDA $C080,Y to turn off the appropriate stepper motor 5. Wait the right amount of time again ...Which is exactly what this routine at $0436 is doing. And since $0433 "falls through" to $0436, it ends up doing this twice. Two half tracks equal one whole track, so calling the routine at $0433 will move the drive head to the next whole track. Now we can go back to $0411 and see how this all fits together. 0411- A2 0F LDX #$0F 0413- A0 00 LDY #$00 0415- 85 27 STA $27 0417- E8 INX 0418- 86 49 STX $49 041A- 84 F9 STY $F9 041C- 98 TYA 041D- 24 4A BIT $4A 041F- 30 03 BMI $0424 0421- B9 63 04 LDA $0463,Y 0424- 85 3D STA $3D 0426- 20 00 04 JSR $0400 0429- A4 F9 LDY $F9 042B- C8 INY 042C- C4 49 CPY $49 042E- 90 EA BCC $041A 0430- A5 27 LDA $27 0432- 60 RTS This clever routine re-uses the drive controller ROM routine to read data into memory. Combined with the manual stepper motor code, it can read any number of tracks, as long as they are monotonically increasing. (It has no logic to go backwards one track.) It even has a lookup table to map between logical and physical sectors, so data can be stored in a "natural" order (by logical sector) during development. If that were the end of the story, it would still be a good story, but it wouldn't have a whole lot to do with copy protection. Data read from disk; film at 11. But remember that invalid address field on track 1? That I noticed in the nibble editor? No sane RWTS would be able to read that track. Any sane RWTS would barf, because the track number listed in the address field doesn't match the track number it was trying to read. That's the entire purpose of the address field, so the RWTS can ensure it's reading data from the correct track and re-adjust the drive head if it's not. But... this track isn't read by a sane RWTS. It's read by a very naive, very minimalist routine embedded in the disk controller card ROM. That routine has none of the usual checking of track numbers because it doesn't need to in order to fulfill its primary purpose (reading track $00). It has already slammed the drive head far enough that it can safely assume it's reading track $00, so it just blindly reads and never double-checks the track numbers in the address field. By manually moving the drive head, the original disk can reuse that naive routine in ROM to read data from intentionally malformed tracks. That's wickedly delicious. With all that said, this is what BOOT1 looks like: 0473- 46 4A LSR $4A ; move the disk head to the next track 0475- 20 33 04 JSR $0433 ; read some stuff into $1500 and up 0478- A9 15 LDA #$15 047A- A2 0A LDX #$0A 047C- 20 13 04 JSR $0413 ; read more stuff into $4000 and up 047F- A9 40 LDA #$40 0481- A2 0F LDX #$0F 0483- 20 15 04 JSR $0415 ; read a bunch more stuff 0486- 20 B7 04 JSR $04B7 ; lots more reading 0489- A9 54 LDA #$54 048B- 20 05 04 JSR $0405 048E- 20 0E 04 JSR $040E 0491- 20 33 04 JSR $0433 ; last one 0494- A2 04 LDX #$04 0496- 20 13 04 JSR $0413 ; turn off disk motor 0499- BD 88 C0 LDA $C088,X ; copy some stuff to page 3 049C- A0 7F LDY #$7F 049E- B9 00 A8 LDA $A800,Y 04A1- 99 00 03 STA $0300,Y 04A4- 88 DEY 04A5- 10 F7 BPL $049E ; set up Applesoft input vector to ; point to the actual start of the game ; at $02A8 (note: $02A8..$02FF was ; originally at $08A8..$08FF in T00,S00 ; and was moved with the rest of page 8 ; during boot0) 04A7- A9 A8 LDA #$A8 04A9- 85 38 STA $38 04AB- A9 02 LDA #$02 04AD- 85 39 STA $39 04AF- A9 60 LDA #$60 04B1- 8D EA 03 STA $03EA ; jump to Applesoft BASIC cold start ; to start the program (eventually ; jumps to ($38) to get input, which ; actually starts the game instead) 04B4- 4C 00 E0 JMP $E000 TRACE2 lets the entire game load itself before jumping to the monitor: ; set up first callback after boot0 96F8- A9 97 LDA #$97 96FA- 8D 18 08 STA $0818 96FD- A9 04 LDA #$04 96FF- 8D 54 08 STA $0854 ; first callback starts here -- ; set up break after boot1 9702- 4C 01 08 JMP $0801 9705- A9 59 LDA #$59 9707- 8D B5 04 STA $04B5 970A- A9 FF LDA #$FF 970C- 8D B6 04 STA $04B6 ; continue boot1 970F- 4C 73 04 JMP $0473 At this point, $0800..$A8FF has the entire game (minus the initialization code in $02A8..$02FF). I can save it as one file and BRUN it. But hi-res page 1 ($2000..$3FFF) is blank; the original disk zapped it and displayed it as a blank screen during boot. So I can use that space to keep the file size to a minimum. *2000<0800.14FFM *2D00<9600.A8FFM *C500G ... ]BSAVE WORDSPINNER DATA,A$1500,L$8100 Now I need to make a loader to move everything back into the proper place and start the game. I'm going to put this at $1400 (just before the data I just saved). It will need to relocate itself before relocating the code in hi-res page 1, since part of that overwrites $1400..$14FF. Hey, let's put it in $0200! Then I can include that extra initialization code from the original game's boot sector in $14A8..$14FF; it will get relocated to $02A8..$02FF, which is exactly where the game expects it. ; relocate the loader 1400- A0 00 LDY #$00 1402- B9 00 14 LDA $1400,Y 1405- 99 00 02 STA $0200,Y 1408- C8 INY 1409- D0 F7 BNE $1402 140B- 4C 0E 02 JMP $020E ; this ends up at $020E ; copy $0800..$14FF into place 140E- A2 0D LDX #$0D 1410- B9 00 20 LDA $2000,Y 1413- 99 00 08 STA $0800,Y 1416- C8 INY 1417- D0 F7 BNE $1410 1419- EE 12 02 INC $0212 141C- EE 15 02 INC $0215 141F- CA DEX 1420- D0 EE BNE $1410 ; copy $9600..$A8FF into place 1422- A2 13 LDX #$13 1424- B9 00 2D LDA $2D00,Y 1427- 99 00 96 STA $9600,Y 142A- C8 INY 142B- D0 F7 BNE $1424 142D- EE 26 02 INC $0226 1430- EE 29 02 INC $0229 1433- CA DEX 1434- D0 EE BNE $1424 ; verbatim from the original disk 1436- A0 7F LDY #$7F 1438- B9 00 A8 LDA $A800,Y 143B- 99 00 03 STA $0300,Y 143E- 88 DEY 143F- 10 F7 BPL $1438 ; original disk did this in boot0, but ; I need to do it here too so the file ; will be BRUN'able from any disk after ; BASIC has already started 1441- 20 93 FE JSR $FE93 ; verbatim from the original disk 1444- A9 A8 LDA #$A8 1446- 85 38 STA $38 1448- A9 02 LDA #$02 144A- 85 39 STA $39 144C- A9 60 LDA #$60 144E- 8D EA 03 STA $03EA 1451- 4C 00 E0 JMP $E000 $14A8..$14FF is verbatim from the original disk's T00,S00. It contains game-specific initialization code. ]BSAVE WORDSPINNER (4AM CRACK), A$1400,L$8200 The final binary file is 133 sectors and can be BRUN from any DOS 3.3 or compatible disk. I've chosen to distribute it with a tiny loader (derived from CompatiBoot) that loads the game as quickly and as silently as possible, but you can copy the binary file to another disk and BRUN it manually. These softdocs are also distributed on the disk, viewable with any text file viewer. Quod erat liberandum. --------------------------------------- A 4am crack No. 16 -------------------EOF-----------------