-------The Electric Crayon: ABCs------- A 4am crack 2016-02-26 --------------------------------------- Name: The Electric Crayon: ABCs Genre: educational Year: 1986 Credits: programming by Brian A. Rice illustrations by Mark Russell Finch Publisher: Polarware, Inc. Media: single-sided 5.25-inch floppy OS: ProDOS 1.1.1 Previous cracks: none Similar cracks: #020 The Electric Crayon: Fun on the Farm ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways COPYA no errors, but copy hangs on title screen -- no grinding, nothing, it just stops with the drive motor off Locksmith Fast Disk Backup ditto EDD 4 bit copy (no sync, no count) ditto Copy ][+ nibble editor nothing suspicious Disk Fixer T00 -> looks like standard ProDOS (bootloader and disk catalog) Why didn't any of my copies work? probably a nibble check in the startup program Next steps: 1. Trace the startup program 2. Find the check and disable it 3. Declare victory (*) (*) take a nap ~ Chapter 1 Hello, I'd Like To Have An Argument On The Stack, Please [S6,D1=non-working copy] [S7,D1=ProDOS hard drive, /A4AMCRACK] ]PR#7 ... ]CAT,S6,D1 /A NAME TYPE BLOCKS MODIFIED *PRODOS SYS 30 18-SEP-84 EC.SYSTEM SYS 12 3-SEP-86 SHAPES BIN 1 EC.MAIN.OBJ BIN 13 19-AUG-86 NOMENUS.TK.ABS BIN 23 13-DEC-85 TEST.FONT BIN 6 7-NOV-85 EC.COLOR.DRIVER BIN 10 16-DEC-85 EC.IMAGE2.SETUP BIN 1 9-DEC-85 EC.SCRIBE.SETUP BIN 1 9-DEC-85 EC.ADSWITCH.OBJ BIN 1 9-DEC-85 EC.JDRIVER.OBJ BIN 3 9-DEC-85 EC.KDRIVER.OBJ BIN 4 9-DEC-85 EC.SCREEN.CLIP BIN 9 12-DEC-85 EC.PARMS BIN 1 19-AUG-86 EC.COLORS BIN 17 1-JUL-86 EC.DIALOG.OBJ BIN 4 19-AUG-86 *PICTURES DIR 3 BLOCKS FREE: 31 BLOCKS USED: 249 ProDOS always runs the first .SYSTEM file in the root directory, which would be EC.SYSTEM. Furthermore, it always loads it at address $2000. So let's do that. ]PREFIX /A ]BLOAD EC.SYSTEM,A$2000,TSYS ]CALL -151 *2000L . . standard ProDOS-y initialization, and . a check for 128K (not shown) . 2085- 20 00 BF JSR $BF00 2088- C7 ??? 2089- E1 22 SBC ($22,X) 208B- AD 91 03 LDA $0391 208E- F0 06 BEQ $2096 The ProDOS entry point at $BF00 pulls its parameters from the addresses AFTER the call, which is why the disassembly looks like it contains invalid opcodes. And in fact, they are invalid, but they are never executed. ProDOS grabs them by pulling the return address off the stack, looking at the bytes immediately after the stack pointer, then adjusting the stack pointer so that control returns to the (real) instruction after the last parameter. The number of parameters varies based on the command (the first byte after the JSR), so you really need to read "Beneath Apple ProDOS" to be able to know what's code and what's data. Here's a cleaned up pseudo-listing that makes it a little easier to follow: ; get current prefix (MLI command $C7) 2085- 20 00 BF JSR $BF00 2088- [C7 E1 22] 208B- AD 91 03 LDA $0391 208E- F0 06 BEQ $2096 ; set prefix (MLI command $C6) 2090- 20 00 BF JSR $BF00 2093- [C6 E4 22] 2096- 20 50 35 JSR $3550 ; displays text-based title screen (not ; shown) 2099- 20 19 24 JSR $2419 209C- A9 BB LDA #$BB 209E- 8D EB 22 STA $22EB 20A1- A9 00 LDA #$00 20A3- 8D EA 22 STA $22EA ; open a file (MLI command $C8) 20A6- 20 00 BF JSR $BF00 20A9- [C8 E7 22] *22E7.22FF 22E7- 03 22E8- F7 22 A0 F0 A0 04 A0 00 ^^^^^ pointer to filename 22F0- 7C FF FF C5 A0 01 A0 06 ^^ name length 22F8- 73 68 61 70 65 73 03 0E ^^^^^^^^^^^^^^^^^ "shapes" 20AC- AD EC 22 LDA $22EC 20AF- 8D EE 22 STA $22EE ; read "shapes" file (MLI command $CA) 20B5- 20 00 BF JSR $BF00 20B8- [CA ED 22] ; close "shapes" file (MLI command $CC) 20BB- 20 00 BF JSR $BF00 20BE- [CC F5 22] ; and call it 20C1- 20 00 7C JSR $7C00 $7C00 isn't part of EC.SYSTEM. It must be loaded from one of the other files. A sector search finds it on T04,S05. T04,S05,$C1 change "20 00 7C" to "4C 59 FF" ]PR#6 ...boots to title screen, then breaks to the monitor... ~ Chapter 2 Do You Know How Fast You Were Going? No, But I Know Exactly Where I Am! Here we go. *7C00L ; save some state from zero page 7C00- A2 03 LDX #$03 7C02- B5 00 LDA $00,X 7C04- 48 PHA 7C05- CA DEX 7C06- 10 FA BPL $7C02 ; put an "RTS" at $00 and call it (WTF) 7C08- A9 60 LDA #$60 7C0A- 85 00 STA $00 7C0C- 20 00 00 JSR $0000 ; do some fancy stack pointer ; manipulation (why?) 7C0F- BA TSX 7C10- CA DEX 7C11- CA DEX 7C12- 9A TXS 7C13- 68 PLA 7C14- 85 00 STA $00 7C16- 68 PLA 7C17- 85 01 STA $01 ; this is a simple decryption loop 7C19- A0 89 LDY #$89 7C1B- A9 FF LDA #$FF 7C1D- 51 00 EOR ($00),Y 7C1F- 91 00 STA ($00),Y 7C21- 88 DEY 7C22- C0 17 CPY #$17 7C24- D0 F5 BNE $7C1B OK, I figured out what it's doing to stack, and why it put an "RTS" at $00 and called it. This entire routine is relocatable. There's no "program counter" on the Apple II. I mean, obviously there is a program counter, but it's not like a simple address that you can read to see what instruction is going to be executed next. But this routine figures out where in memory it lives, by creating a routine that does nothing, calling it, and taking advantage of the fact that the "stack" is just a page of memory (from $0100..$01FF) and a register that is an index into that page. After the call to $0000 (at $7C0C), the return address is still somewhere in the $0100..$01FF range. (It's not overwritten until you call a different subroutine or do something else that pushes bytes to the stack.) This routine figures out where the return address is in the stack page and stores it in $00/$01. Then the loop at $7C19 decrypts memory backwards from that address + $89, back to that address + $17, then stops. $17 bytes is the size of the code that manipulates the stack pointer, plus the decryption loop itself. Anyway, $7C26 and beyond is encrypted. The loop at $7C19 decrypts it. I'll need to decrypt it manually. *7C19:4C 59 FF *7C00G *00.01 0000- 0E 7C I need to reproduce the decryption loop somewhere else so I can stop after the loop completes. I'll put this at $0300: 0300- A9 0E LDA #$0E 0302- 85 00 STA $00 0304- A9 7C LDA #$7C 0306- 85 01 STA $01 0308- A0 89 LDY #$89 030A- A9 FF LDA #$FF 030C- 51 00 EOR ($00),Y 030E- 91 00 STA ($00),Y 0310- 88 DEY 0311- C0 17 CPY #$17 0313- D0 F5 BNE $030A 0315- 60 RTS *300G Now I should be able to see the decrypted code. ~ Chapter 3 Timing Is Everything *7C26L ; get slot number (x16) from ProDOS 7C26- AE 30 BF LDX $BF30 ; turning on the disk motor manually: ; never not suspicious 7C29- BD 89 C0 LDA $C089,X ; initialize Death Counters, probably 7C2C- A9 56 LDA #$56 7C2E- 85 03 STA $03 7C30- A9 08 LDA #$08 7C32- C6 02 DEC $02 7C34- D0 04 BNE $7C3A 7C36- C6 03 DEC $03 ; if Death Counter hits zero, branch 7C38- F0 59 BEQ $7C93 ; look for an $FB nibble 7C3A- BC 8C C0 LDY $C08C,X 7C3D- 10 FB BPL $7C3A 7C3F- C0 FB CPY #$FB 7C41- D0 ED BNE $7C30 7C43- F0 00 BEQ $7C45 ; kill a few cycles (not pointless, ; because the disk spins independently ; of the CPU, so all of these low-level ; disk reads are highly time-sensitive) 7C45- EA NOP 7C46- EA NOP ; read data latch (note: no BPL loop ; here, we're just reading it once) 7C47- BC 8C C0 LDY $C08C,X ; do a compare to set or clear the ; carry bit (among other things, but ; it's the carry bit we care about) 7C4A- C0 08 CPY #$08 ; rotate the carry into the low bit of ; the accumulator 7C4C- 2A ROL ; if we just rolled a "1" bit out of ; the high bit of the accumulator, take ; this branch 7C4D- B0 0B BCS $7C5A ; next nibble needs to be $FF 7C4F- BC 8C C0 LDY $C08C,X 7C52- 10 FB BPL $7C4F ; ...otherwise we start over 7C54- C0 FF CPY #$FF 7C56- D0 D8 BNE $7C30 ; loop back to get next nibble 7C58- F0 EB BEQ $7C45 ; execution continues here (from $7C4D) ; if the accumulator is anything but ; %00001010, start over 7C5A- C9 0A CMP #$0A 7C5C- D0 D2 BNE $7C30 ; next nibbles must be "D5 AA 96 AA AB" 7C5E- BD 8C C0 LDA $C08C,X 7C61- 10 FB BPL $7C5E 7C63- C9 D5 CMP #$D5 7C65- D0 C9 BNE $7C30 7C67- F0 00 BEQ $7C69 7C69- BD 8C C0 LDA $C08C,X 7C6C- 10 FB BPL $7C69 7C6E- C9 AA CMP #$AA 7C70- D0 BE BNE $7C30 7C72- F0 00 BEQ $7C74 7C74- BD 8C C0 LDA $C08C,X 7C77- 10 FB BPL $7C74 7C79- C9 96 CMP #$96 7C7B- D0 B3 BNE $7C30 7C7D- F0 00 BEQ $7C7F 7C7F- BD 8C C0 LDA $C08C,X 7C82- 10 FB BPL $7C7F 7C84- C9 AA CMP #$AA 7C86- D0 A8 BNE $7C30 7C88- F0 00 BEQ $7C8A 7C8A- BD 8C C0 LDA $C08C,X 7C8D- 10 FB BPL $7C8A 7C8F- C9 AB CMP #$AB 7C91- D0 9D BNE $7C30 I got lost several times trying to follow this routine. I think the easiest way to explain it is to show the difference between the original disk and my non-working copy. Here is the original disk, as seen by the Copy II+ nibble editor. Nibbles with extra "0" bits (timing bits) after them are displayed in inverse on an original machine, marked here with a "+" after the nibble. --v-- COPY ][ PLUS BIT COPY PROGRAM 8.4 (C) 1982-9 CENTRAL POINT SOFTWARE, INC. --------------------------------------- TRACK: START: 1B1E LENGTH: 17C1 1C70: 9F EB E5 FC D7 D7 D7 EE VIEW 1C78: FA E6 E6 FF FE F2 ED FD 1C80: FF EF ED BA BB DD AF E6 1C88: B7 A7 CB B7 DE AA EB FF 1C90: FF FF FF FB+FF FF+FF FF+ 1C98: FD FF+FF+FF+FF+FF+FF+FF+ 1CA0: FF+FF+D5 AA 96 AA AB AA 1CA8: AA AA AB AA AA AB FF FF+ 1CB0: FF+FF+FF+FF+FF+FF D5 AA --------------------------------------- A TO ANALYZE DATA ESC TO QUIT ? FOR HELP SCREEN / CHANGE PARMS Q FOR NEXT TRACK SPACE TO RE-READ --^-- It's easy to understand why a simple sector copy failed. The sequence that this code is looking for starts at offset $1C93, which is between the end of one sector and the beginning of the next. (The data epilogue is at $1C8C; the next address prologue is at $1CA2.) Sector copiers discard everything between those delimiters and rebuild the track with a default pattern of sync bytes. That pattern doesn't include an $FB nibble, so the nibble check fails. But the EDD bit copy also failed. Here is the original disk's pattern at offset $1C93: - $FB + timing bit - $FF - $FF + timing bit - $FF - $FF + timing bit And here is what the same part of the track looks like on my failed EDD copy: --v-- COPY ][ PLUS BIT COPY PROGRAM 8.4 (C) 1982-9 CENTRAL POINT SOFTWARE, INC. --------------------------------------- TRACK: START: 1B1E LENGTH: 17C1 1C70: 9F EB E5 FC D7 D7 D7 EE VIEW 1C78: FA E6 E6 FF FE F2 ED FD 1C80: FF EF ED BA BB DD AF E6 1C88: B7 A7 CB B7 DE AA EB FF 1C90: FF FF FF FB+FF FF FF+FF+ 1C98: FD FF+FF+FF+FF+FF+FF+FF+ 1CA0: FF+FF+D5 AA 96 AA AB AA 1CA8: AA AA AB AA AA AB FF FF+ 1CB0: FF+FF+FF+FF+FF+FF D5 AA --------------------------------------- A TO ANALYZE DATA ESC TO QUIT ? FOR HELP SCREEN / CHANGE PARMS Q FOR NEXT TRACK SPACE TO RE-READ --^-- A subtle difference! The sequence at offset $1C93 now looks like this: - $FB + timing bit - $FF - $FF - $FF + timing bit - $FF + timing bit This code is looking for $FF bytes with an alternating pattern of timing bit, no timing bit, timing bit, no timing bit. The accumulator holds the pattern of whether each sync byte had a timing bit after it. It's set one bit at a time, rotated into place from the carry bit that was set by the "CPY #$08" that happened after getting the value of the data latch (LDY $C08C,X) that happened after doing just enough NOPs that the value of the data latch will depend on the presence of a timing bit after the previous nibble. Which is brilliant. Anyway, if the value of the accumulator (i.e. the pattern of timing bits) is wrong, the program knows it's not running on an original disk. ~ Chapter 4 One Byte To Rule Them All, And In The Darkness Patch Them Continuing from $7C93... ; turn off drive motor ; (hey, this was also the failure path, ; which means the real success/failure ; check is later) 7C93- DD 88 C0 CMP $C088,X ; re-encrypt the protection check 7C96- A0 89 LDY #$89 7C98- A9 FF LDA #$FF 7C9A- 51 00 EOR ($00),Y 7C9C- 91 00 STA ($00),Y 7C9E- 88 DEY 7C9F- C0 17 CPY #$17 7CA1- D0 F5 BNE $7C98 ; check the Death Counter 7CA3- A4 03 LDY $03 ; restore zero page state 7CA5- A2 00 LDX #$00 7CA7- 68 PLA 7CA8- 95 00 STA $00,X 7CAA- E8 INX 7CAB- E0 04 CPX #$04 7CAD- D0 F8 BNE $7CA7 ; ah! if the nibble check failed, zero ; page $03 will be 0, which means this ; will fall into an infinite loop at ; $7CB0 7CAF- 98 TYA 7CB0- F0 FE BEQ $7CB0 ; WARNING: side effects detected! 7CB2- A9 00 LDA #$00 7CB4- 8D D0 9A STA $9AD0 7CB7- A9 C6 LDA #$C6 7CB9- 8D D1 9A STA $9AD1 7CBC- 60 RTS If the protection check fails, we get into an infinite loop (at $7CB0) and this routine never returns. That explains the behavior I saw on my non- working copy. But if the protection check succeeds, we set two seemingly arbitrary memory locations. Experience has taught me that side effects of protection checks are never innocuous, so I'll need to be sure that my crack sets them too (while bypassing all the protect-y bits of course). Backing up to the beginning of the protection code, I see this branch, on the low byte of the 16-bit Death Counter: 7C32- C6 02 DEC $02 7C34- D0 04 BNE $7C3A If I changed that branch to go straight to the exit path (at $7C93), the high byte (zp$03) would still be non-zero, and the protection check would succeed immediately. Then I can piggyback on the existing post-check side effects instead of having to recreate them. *7C35:5D *7C32L 7C32- C6 02 DEC $02 7C34- D0 5D BNE $7C93 That's the code I want. One problem: this code is encrypted on disk. Well, the decryption loop is the same as the encryption loop (it just EORs everything with a constant), so I can re-run my decryption loop to re-encrypt the code with my "immediately branch to $7C93" patch. *300G ; re-encrypt with new code *7C30.7C35 7C30- 56 F7 39 FD 2F A2 ^^ new encrypted value Turning to my trusty Disk Fixer sector editor, I search for the sequence of encrypted opcodes before the byte I want to change: [Disk Fixer] --> [F]ind --> [H]ex --> "56 F7 39 FD 2F" And I find it on T06,S0D. (Copy II Plus confirms that this is part of the "SHAPES" file, so everything lines up.) T06,S0D,$35 change "FB" to "A2" ]PR#6 ...boots to title screen, then breaks to the monitor... Oops, I forgot to undo my debugging breakpoint that I set earlier. T04,S05,$C1 change "4C 59 FF" back to "20 00 7C" ]PR#6 ...works... Quod erat liberandum. --------------------------------------- A 4am crack No. 622 ------------------EOF------------------