--------------Muppet Slate------------- A 4am crack 2017-04-24 --------------------------------------- Name: Muppet Slate Version: 1.0 Genre: educational Year: 1988 Authors: Tad Wood, Scott Clough, Donna Stanger Publisher: Sunburst Communications Platform: Apple ][+ or later Media: 2 single-sided 5.25-inch disks OS: ProDOS 1.4 QB Previous cracks: none Similar cracks: #113 1-2-3 Sequence Me ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways COPYA No errors, but the copy boots ProDOS, displays a company logo, then quickly switches to a graphical error message "Unable to load Muppet Slate. Press RETURN to reboot." Locksmith Fast Disk Backup ditto EDD 4 bit copy (no sync, no count) ditto Copy ][+ nibble editor nothing on track $23, no obvious oddities elsewhere Disk Fixer T00,S00 looks like standard ProDOS bootloader. Track $00 has a disk catalog, and setting "DOS type" to "[P]RODOS" allows me to see a full directory listing and follow individual files. Nothing suspicious. Why didn't any of my copies work? Probably a nibble check during boot. There's nothing wrong with the copy except that it's, you know, not an original. Something is going out of its way to display a pretty graphical error message to tell me to go f--- myself. Next steps: 1. Search for common markers of a nibble check 2. If that fails, trace the boot through the startup program 3. If that fails, I dunno, go feed the ducks or something? ~ Chapter 1 In Which We Get Lucky 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 need to 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/$0E-$A7 $04/$0D-$5C $05/$0A-$3C $05/$0B-$AF $11/$0B-$C1 --^-- T00,S0E is part of the bootloader. T04,S0D appears to be part of ProDOS. T05,S0A and T05,S0B are suspicious. According to Copy II Plus "disk map," these two sectors are part of the file MS.SYSTEM, which is coincidentally the first .SYSTEM file on the disk and would be executed first after ProDOS loads. --v-- DISK MAP SLOT 6 DRIVE 1 /MUPPET.SLATE.A/MS.SYSTEM TRACK 1 2 0123456789ABCDEF0123456789ABCDEF012 S0 .....**............................ EE .....**............................ CD .....**............................ TC .....**............................ OB .....**............................ RA .....**............................ 9 .....**............................ 8 .....**............................ 7 .....**............................ 6 .....**............................ 5 .....**............................ 4 .....**............................ 3 .....*............................. 2 .....*............................. 1 .....*............................. F .....*............................. USE ARROW KEYS TO MAP OTHER FILES --^-- Rebooting to my hard drive in slot 7 and dropping to a BASIC prompt, I can load MS.SYSTEM into memory and start poking around. ~ Chapter 2 Who Needs Device Independence Anyway? [S7,D1=my hard drive, /A4AMCRACK/] [S6,D1=non-working copy] ]PR#7 ... ; Copy II Plus showed the disk volume ; name as "MUPPET.SLATE.A" ]PREFIX /MUPPET.SLATE.A ; ProDOS always loads .SYSTEM files at ; address $2000 ]BLOAD MS.SYSTEM,A$2000,TSYS ]CALL -151 *2000L ; immediately copies itself to higher ; memory 2000- A2 1A LDX #$1A 2002- A0 00 LDY #$00 2004- B9 20 20 LDA $2020,Y 2007- 99 00 60 STA $6000,Y 200A- C8 INY 200B- D0 F7 BNE $2004 200D- EE 06 20 INC $2006 2010- EE 09 20 INC $2009 2013- CA DEX 2014- D0 EE BNE $2004 ; and we continue from there 2016- 4C 00 60 JMP $6000 ; replace "JMP $6000" with "RTS" *2016:60 ; execute memory copy *2000G *6000L ; copies some code to $0350 and sets ; the reset vector to point to it ; (not shown) 6000- 20 3C 61 JSR $613C ; check machine ID (not shown) 6003- 20 CE 60 JSR $60CE ; load and display Sunburst logo ; (not shown, but the subroutine is ; self-contained and I can call it ; directly from the monitor and it ; displays the logo then exits ; gracefully) 6006- 20 BE 61 JSR $61BE ; standard ProDOS initialization ; (not shown) 6009- 20 B8 60 JSR $60B8 ; hmm 600C- 20 05 62 JSR $6205 *6205L ; initialize counter 6205- A9 03 LDA #$03 6207- 8D 29 64 STA $6429 ; try something 620A- 20 16 62 JSR $6216 ; if carry is clear, branch ahead and ; exit to caller 620D- 90 06 BCC $6215 ; otherwise decrement the counter and ; try again 620F- CE 29 64 DEC $6429 6212- D0 F6 BNE $620A ; if counter reached 0, set carry and ; exit to caller 6214- 38 SEC 6215- 60 RTS So whatever is happening at $6216, we get three chances for it to succeed. That's... somewhat suspicious. *6216L 6216- A9 00 LDA #$00 6218- 8D 24 64 STA $6424 621B- A9 08 LDA #$08 621D- 8D 25 64 STA $6425 6220- AD 30 BF LDA $BF30 6223- 8D 23 64 STA $6423 6226- 29 7F AND #$7F 6228- 85 0C STA $0C 622A- 20 E1 63 JSR $63E1 *63E1L ; munge the boot slot into $Cx form ; (so boot from slot 6 -> $C6) 63E1- AA TAX 63E2- 29 70 AND #$70 63E4- 4A LSR 63E5- 4A LSR 63E6- 4A LSR 63E7- 4A LSR 63E8- 18 CLC 63E9- 69 C0 ADC #$C0 63EB- 8D 19 64 STA $6419 63EE- A0 01 LDY #$01 63F0- A9 20 LDA #$20 63F2- 20 17 64 JSR $6417 *6417L 6417- D9 00 C1 CMP $C100,Y 641A- 60 RTS OK, I just figured it out. This routine is checking for magic bytes in the card firmware to detect whether we booted from a 5.25-inch floppy drive. The magic bytes to check are $Cx01 = #$20 $Cx03 = #$00 $Cx05 = #$03 $CxFF = #$00 We self-modified the instruction at $6417 based on the boot slot, so it's comparing against $C600,Y if we booted from slot 6, $C500,Y if we booted from slot 5, &c. Lots of protection checks only work if you boot from slot 6, so I appreciate the effort, kind of. 63F5- D0 1D BNE $6414 ; check $Cx03 63F7- A0 03 LDY #$03 63F9- A9 00 LDA #$00 63FB- 20 17 64 JSR $6417 63FE- D0 14 BNE $6414 ; check $Cx05 6400- A0 05 LDY #$05 6402- A9 03 LDA #$03 6404- 20 17 64 JSR $6417 6407- D0 0B BNE $6414 ; check $CxFF 6409- A0 FF LDY #$FF 640B- A9 00 LDA #$00 640D- 20 17 64 JSR $6417 6410- D0 02 BNE $6414 ; carry the clear bit if the boot slot ; was identified as a 5.25-inch floppy ; drive 6412- 18 CLC 6413- 60 RTS 6414- 8A TXA ; set the carry bit otherwise 6415- 38 SEC 6416- 60 RTS Continuing from $622D... 622D- 90 22 BCC $6251 . . [some code omitted, but basically, . booting from anything but a floppy . drive is always treated as an error] . 6251- 4C 6E 62 JMP $626E And away we go. ~ Chapter 3 It's Not A Phase, Mom, This Is Who I Am Via the unconditional jump from $6251, execution continues at $626E: *626EL ; save registers 626E- 08 PHP 626F- 78 SEI 6270- A9 03 LDA #$03 6272- 8D 26 64 STA $6426 6275- A9 00 LDA #$00 6277- 8D 27 64 STA $6427 627A- 20 1B 64 JSR $641B *641BL 641B- 20 00 BF JSR $BF00 641E- [80] ; MLI command 641F- [22 64] ; MLI parameter table 6421- 60 RTS Ah! A raw block read. Never a good sign (outside of the PRODOS system file). And if the MLI parameter table is at $6422, then we just set it to read block 3 at $6270. Returning to the caller and continuing from $627D: ; try one thing (more on this in a ; moment) 627D- 20 1A 63 JSR $631A ; if that works, return to caller ; (I'm assuming here that we're keeping ; with the convention of "C clear = ; success, C set = failure" that the ; other routines were using.) 6280- 90 08 BCC $628A ; try another thing 6282- 20 8D 62 JSR $628D ; if that works, return to caller 6285- 90 03 BCC $628A ; otherwise restore registers and set ; carry for caller 6287- 28 PLP 6288- 38 SEC 6289- 60 RTS ; (success path, from $6280 or $6285) ; restore registers and clear carry for ; caller 628A- 28 PLP 628B- 18 CLC 628C- 60 RTS So there are two different things, and either one of them can succeed, and that's enough. But if they both fail, then the caller gets the failure signal of the carry bit set. Here's the first thing, at $631A: ; turn on the drive motor manually ; (this is what led me to this code in ; the first place) 631A- A6 0C LDX $0C 631C- BD 89 C0 LDA $C089,X 631F- BD 8E C0 LDA $C08E,X ; munge the boot slot, e.g. $C6 -> $EC, ; presumably so we can access the data ; latch ($C0EC) in a slot-independent ; fashion 6322- 18 CLC 6323- A5 0C LDA $0C 6325- 69 8C ADC #$8C 6327- 8D 3B 63 STA $633B 632A- 8D A4 63 STA $63A4 ; initialize Death Counter 632D- A0 F0 LDY #$F0 632F- 8C 28 64 STY $6428 6332- C8 INY 6333- D0 05 BNE $633A 6335- EE 28 64 INC $6428 ; when Death Counter hits 0, branch to ; failure path 6338- F0 62 BEQ $639C ; find address prologue "D5 AA 96" 633A- AD EC C0 LDA $C0EC 633D- 10 FB BPL $633A 633F- C9 D5 CMP #$D5 6341- D0 EF BNE $6332 6343- 20 A3 63 JSR $63A3 6346- C9 AA CMP #$AA 6348- D0 F5 BNE $633F 634A- 20 A3 63 JSR $63A3 634D- C9 96 CMP #$96 634F- D0 EE BNE $633F 6351- 20 A3 63 JSR $63A3 *63A3L ; read a nibble and return it 63A3- AD EC C0 LDA $C0EC 63A6- 10 FB BPL $63A3 63A8- 60 RTS Ah, this was one of the instructions we self-modified earlier (at $632A). Continuing from $6354... ; I guess we're ignoring that nibble we ; just read, because now we're reading ; another one (which we also ignore). 6354- 20 A3 63 JSR $63A3 ; The next 4 nibbles need to be $AA. ; Since we're inside an address field ; and just skipped 2 nibbles, that ; means we're looking for T00,S00. 6357- A2 03 LDX #$03 6359- 20 A3 63 JSR $63A3 635C- C9 AA CMP #$AA 635E- D0 D2 BNE $6332 6360- CA DEX 6361- 10 F6 BPL $6359 ; skip 2 more nibbles (address field ; checksum) 6363- 20 A3 63 JSR $63A3 6366- 20 A3 63 JSR $63A3 ; next nibble needs to be $DE (standard ; first nibble of the address epilogue) 6369- 20 A3 63 JSR $63A3 636C- C9 DE CMP #$DE 636E- D0 C2 BNE $6332 ; next nibble needs to be $AA (standard ; second nibble of epilogue) 6370- A6 0C LDX $0C 6372- BD 8C C0 LDA $C08C,X 6375- 10 FB BPL $6372 6377- C9 AA CMP #$AA 6379- D0 B7 BNE $6332 637B- A0 02 LDY #$02 ; reset data latch (!) 637D- BD 8D C0 LDA $C08D,X ; burn some CPU cycles (7 to be exact) 6380- 48 PHA 6381- 68 PLA ; now look for a $BB nibble 6382- BD 8C C0 LDA $C08C,X 6385- 10 FB BPL $6382 6387- C9 BB CMP #$BB 6389- F0 06 BEQ $6391 ; must find that $BB nibble within the ; next 3 nibbles after the epilogue 638B- 88 DEY 638C- 10 F4 BPL $6382 ; otherwise jump back to increment the ; Death Counter 638E- 4C 35 63 JMP $6335 ; execution continues here (from the ; "BEQ" at $6389, after we find the $BB ; nibble) -- ; next nibble must be $F9 6391- BD 8C C0 LDA $C08C,X 6394- 10 FB BPL $6391 6396- C9 F9 CMP #$F9 6398- D0 F4 BNE $638E ; success path falls through to here -- ; clear carry on the way out 639A- 18 CLC ; there's a hidden "SEC" instruction ; here (opcode $38) which is called if ; the Death Counter hits 0 ($6338 ; branches to $639C, in the middle of ; this instruction as listed) 639B- 24 38 BIT $38 ; either way, turn off the drive motor ; on the way out 639D- A6 0C LDX $0C 639F- BD 88 C0 LDA $C088,X 63A2- 60 RTS OK, what the hell is going on? ~ Chapter 4 The Truth Is In The Bits Turning to my trusty Copy II Plus nibble editor, I can easily look at track 0 and find the nibbles that this routine is checking by searching for "AA AA AA": --v-- COPY ][ PLUS BIT COPY PROGRAM 8.4 (C) 1982-9 CENTRAL POINT SOFTWARE, INC. --------------------------------------- TRACK: 00 START: 1800 LENGTH: 3DFF 26E8: FF+FF+FF+FF+FF+FF+FF+FF+ VIEW 26F0: FF+FF+FF+FF+FF+FF+FF+FF+ 26F8: FF+FF+FF+FF+FF+FF+FF+FF+ 2700: FF+FF+FF+FF+FF+FF+FF+FF+ 2708: D5 AA 96 AA AB AA AA AA <-270D 2710: AA AA AB DE AA EB+97+DF+ 2718: FF E7 F9 FE FF+FF+D5 AA 2720: AD AC B6 ED F2 FF 9E B7 FIND: 2728: FB D9 F9 FD F6 F9 DA EF AA AA AA --------------------------------------- A TO ANALYZE DATA ESC TO QUIT ? FOR HELP SCREEN / CHANGE PARMS Q FOR NEXT TRACK SPACE TO RE-READ --^-- Nibbles with an extra "0" bit (called a "timing bit") after them are normally displayed in inverse. For the purposes of plain text, I've reformulated them as "+" signs instead. Taking it nibble by nibble, starting at offset $2708, we 1. verify the address field prologue ($D5 $AA $96) 2. skip the disk volume number (2 nibbles) 3. verify the track + sector number ($AA $AA $AA $AA) 4. skip the address field checksum (2 nibbles) 5. verify the address field epilogue ($DE $AA) That brings us to offset $2715, where I see $EB, $97, $DF, $FF, $E7, $F9... But no $BB. What would that nibble sequence look like on disk? The answer is, "It depends." Here is the simplest possible answer: |--EB--||--97--||--DF--||--FF--| 11101011100101111101111111111111 But wait. Every nibble read from disk must have its high bit set. In theory, you could insert one or two "0" bits after any of those nibbles. (Two is the maximum, due to hardware limitations.) These "timing bits" would be swallowed by the standard "wait for data latch to have its high bit set" loop, which you see over and over in any RWTS code: :1 LDA $C08C,X BPL :1 And in fact, Copy II Plus is smart enough to notice that some of those nibbles had a timing bit after them. So maybe the bitstream looks like this: |--EB--| |--97--| |--DF--| |--FF--| 11101011010010111011011111011111111 ^ ^ ^ (extra) (extra) (extra) $EB is followed by 1 timing bit, $97 is followed by 1 timing bit, and $DF is followed by 1 timing bit. Totally legal, works on any Apple II computer and any floppy drive. A normal "LDA $C08C,X; BPL" loop would still interpret this bitstream as "EB 97 DF". Each of the extra "0" bits appear just after we've just read a nibble and we're waiting for the high bit to be set again. So they get "swallowed." Ignored. Like they were never there. But what if we throw away the first few bits of this bitstream, then restart the bit-to-nibble interpretation? We can do this by resetting the data latch (LDA $C08D,X), which throws away any bits that have been read up to that point and starts over at 0. During the time between reading the $AA nibble and resetting the data latch, the disk is still spinning and bits are accumulating in the data latch. But as soon as we reset the data latch, those bits are lost. How many bits? Let's count cycles. 6372- BD 8C C0 LDA $C08C,X 6375- 10 FB BPL $6372 ; 2 6377- C9 AA CMP #$AA ; 2 6379- D0 B7 BNE $6332 ; 2 637B- A0 02 LDY #$02 ; 2 637D- BD 8D C0 LDA $C08D,X Each bit takes 4 CPU cycles to go by as the disk spins. After 8 cycles, we've missed the first 2 bits of the $EB nibble (marked below with an X): (normal start) |--EB--| |--97--| |--DF--| |--FF--| 11101011010010111011011111011111111 XX|--AD--| |--BB--| |--FB--| (delayed start) After resetting the data latch, the stream is interpreted as a completely different nibble sequence. Some of those "extra" bits are no longer being ignored. The timing bit after $EB is now being interpreted as data, as part of the $AD nibble. Other bits (inside $97) now occur in between the $AD and $BB nibbles, so they get "swallowed." One of the bits that was part of the $DF nibble is now being swallowed as well. And there's our $BB nibble. But this still isn't enough. The nibble after it is $FB, not $F9, so the protection check would still fail. But! Remember that each nibble can have up to two extra "0" bits after it. What if the original disk had two timing bits after the $DF nibble, instead of one? (normal start) +--- here v |--EB--| |--97--| |--DF--| |--FF--| 111010110100101110110111110011111111 XX|--AD--| |--BB--| |--F9--| (delayed start) Aha! Now the desynchronized nibble stream goes $AD... $BB... $F9! And there's the $F9 nibble we needed. So the protection check only passes if $EB is followed by 1 timing bit, $97 is followed by 1 timing bit, and $DF is followed by 2 timing bits. Here's the kicker: even the best bit copiers couldn't tell the difference between 1 timing bit and 2 timing bits. There just isn't enough time to do it at 1 MHz while the disk is spinning independently of the CPU. By wasting just the right number of CPU cycles at just the right point in a specially crafted bitstream, developers could determine at runtime whether you had an original disk. And even the best bit copiers couldn't fool it. ~ Chapter 5 In Which We Get A Second Chance And Still Fail Backing up to $626E, you may recall we were in a loop. If this routine at $631A failed, we called another routine at $628D. And if that failed, we tried each of them again, up to three times, before finally setting the carry (to indicate failure) and returning to the caller. It turns out that $628D is another nibble desynchronization routine, but with slightly different timing after we reset the data latch. Here is the only substantial difference: 62EE- A0 02 LDY #$02 62F0- BD 8D C0 LDA $C08D,X 62F3- BD 8C C0 LDA $C08C,X 62F6- 10 FB BPL $62F3 62F8- C9 BB CMP #$BB 62FA- F0 06 BEQ $6302 62FC- 88 DEY 62FD- 10 F4 BPL $62F3 62FF- 4C A8 62 JMP $62A8 The timing of the desynchronization is slightly tighter. After resetting the data latch (at $62F0), it immediately checks for the $BB nibble, instead of burning an additional 7 CPU cycles on PHA + PLA. Needless to say, a copy is not going to pass this check either. It still relies on desynchronizing the bitstream by resetting the data latch, throwing away two bits of the $EB nibble and interpreting the remaining bitstream. It still requires the same 2 timing bits after the $DF nibble, and still, the most advanced bit copier would not be able to reproduce the bitstream. Backing all the way up to $6205, I want this entire routine to clear the carry and return gracefully. No checking if we booted from a floppy drive. No fancy nibble desynchronization. No try-three- times-before-dying. Just CLC and RTS. T05,S04,$25: A903 -> 1860 ]PR#6 ...works, and it is glorious... As an added bonus, since we removed the floppy drive check, you can now copy all these files to a ProDOS hard drive and run the program from there. Quod erat liberandum. ~ Acknowledgments Thanks to qkumba for reviewing drafts of this write-up. --------------------------------------- A 4am crack No. 1165 ------------------EOF------------------