----Paper Models: The Christmas Kit---- A 4am crack 2015-02-20 -------------------. updated 2015-09-10 |___________________ Name: Paper Models: The Christmas Kit Genre: graphics Year: 1986 Authors: Carol Manley, Ivan Manley, Robert C. Clardy Publisher: Activision Media: single-sided 5.25-inch floppy OS: DOS 3.3 Other versions: none (preserved here for the first time) ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways COPYA no read errors, but copy boots DOS, swings to high track, then crashes Locksmith Fast Disk Backup ditto EDD 4 bit copy (no sync, no count) ditto Copy ][+ nibble editor nothing suspicious Disk Fixer looks like unmodified DOS 3.3 master (loads in low memory then relocates) T11 -> regular disk catalog T01,S09 -> startup program is "PM1*" Why didn't my copies work? Probably a nibble check called by the startup program. Activision loves "invisible" nibble checks that don't fail immediately but have a side effect. Later code relies on the side effect and crashes spectacularly. To verify my intuition that the DOS is unmodified (and the copy protection is self-contained somewhere else), I'll boot from a DOS 3.3 master disk and try to run the program. [S6,D1=original disk] [S5,D1=DOS 3.3 master disk] ]PR#5 ... ]RUN PM1*,S6 ...works... [S6,D1=non-working copy] [S5,D1=DOS 3.3 master disk] ]PR#5 ... ]RUN PM1*,S6 ...fails identically to booting the non-working copy directly... Next steps: 1. Trace the startup program ("PM1*") 2. Find the nibble check and document any side effects 3. Disable the nibble check while maintaining the side effects ~ Chapter 1 In Which It All Starts So Innocently And All Goes Wrong So Quickly [S6,D1=original disk] [S5,D1=DOS 3.3 master disk] ]PR#5 ]CATALOG,S6,D1 DISK VOLUME 254 *A 002 PMPAINT *A 002 PM1* *B 007 PM2* THE *B 024 PM3* CHRISTMAS *A 009 PM4* KIT T 002 PM5* *B 026 PM6* FILE *B 023 PM7* NAMES *A 033 PM8* *B 016 ART1 *B 007 BASKET *B 005 GIFTBAG1 *B 007 GIFTBAG2 *B 011 CART *B 005 STAR *B 007 SOLDIER1 *B 005 SOLDIER2 *B 005 SLEIGH1 *B 005 SLEIGH2 *B 006 SLEIGH3 *B 005 CUBEBOX1 *B 004 CUBEBOX2 *B 007 DIAMBOX1 *B 006 DIAMBOX2 *B 005 HOUSBOX1 *B 004 HOUSBOX2 *B 005 LONGBOX1 *B 004 LONGBOX2 *B 007 TRIBOX1 *B 006 TRIBOX2 *B 005 SMALLBOX *B 005 OBLONG *B 007 LPYRAMID *B 006 DIAMOND *B 004 CUBE *B 007 PRISM *B 006 TALL *B 005 SPYRAMID *B 007 CONE1 *B 004 CONE2 *B 011 BOOKS1 *B 012 BOOKS2 *B 008 BOOKS3 *B 003 BOOKS4 *B 012 TAVERN1 *B 008 TAVERN2 *B 011 TAVERN3 *B 010 TAVERN4 *B 014 SCROOGE1 *B 006 SCROOGE2 *B 004 SCROOGE3 *B 006 SCROOGE4 *B 011 TEA1 *B 005 TEA2 *B 004 TEA3 *B 004 TEA4 *B 011 ENGINE1 *B 009 ENGINE2 *B 007 ENGINE3 *B 007 COALCAR1 *B 005 COALCAR2 *B 009 GRAINCAR *B 008 CABOOSE1 *B 008 CABOOSE2 ]LOAD PM*1 ]LIST 2 D$ = CHR$ (4): PRINT D$"PR#0" : TEXT : HOME 20 PRINT D$"BLOAD PM2* THE" 30 A = PEEK ( - 16255):A = PEEK ( - 16255) 40 PRINT D$"BLOAD PM3* CHRISTMA S,A$D000" 50 POKE 230,64 60 CALL 4864 66 POKE 49236,0: POKE 49234,0: POKE 49239,0: POKE 49232,0 70 POKE 49237,0 100 POKE 104,96: POKE 24576,0: PRINT CHR$ (4)"RUNPM4* KIT" ]BLOAD PM2* THE ]CALL -151 *AA72.AA73 ; last BLOAD address AA72- 00 13 That makes sense. Line 60 calls 4864, which is $1300. *1300L 1300- 4C 00 15 JMP $1500 *1500L ; set reset vector 1500- A9 02 LDA #$02 1502- 8D F2 03 STA $03F2 1505- A9 17 LDA #$17 1507- 8D F3 03 STA $03F3 150A- 49 A5 EOR #$A5 150C- 8D F4 03 STA $03F4 ; maybe an address? 150F- A9 05 LDA #$05 1511- 85 FA STA $FA 1513- A9 17 LDA #$17 1515- 85 FB STA $FB ; suspicious 1517- A9 C5 LDA #$C5 1519- 48 PHA ; memory move 151A- A9 00 LDA #$00 151C- 85 FC STA $FC 151E- A2 03 LDX #$03 1520- BC 3D 15 LDY $153D,X 1523- 91 FA STA ($FA),Y 1525- CA DEX 1526- 10 F8 BPL $1520 ; suspicious 1528- 8A TXA 1529- 48 PHA We've now pushed $C5/$FF to the stack, so an RTS right now would jump to $C600 and reboot slot 6. 152A- 20 41 15 JSR $1541 *1541L ; call RWTS to position drive head 1541- A5 FB LDA $FB 1543- A4 FA LDY $FA 1545- 20 B5 B7 JSR $B7B5 1548- A9 00 LDA #$00 154A- 85 48 STA $48 154C- 90 02 BCC $1550 ; on error, pop the real return address ; and leave $C5/$FF at the top of the ; stack, then RTS to reboot 154E- 68 PLA 154F- 68 PLA 1550- 60 RTS *152DL ; get slot number (x16) into X 152D- A0 01 LDY #$01 152F- 8C 11 17 STY $1711 1532- B1 FA LDA ($FA),Y 1534- AA TAX ; here we go 1535- 20 51 15 JSR $1551 We're closing in on the copy protection routine. Can you feel it? I swear I can feel it. The random PHA instructions are a good clue. Disk seeks for no apparent reason. The no-second-chances approach to error handling. This is unfriendly territory. ~ Chapter 2 In Which We Forge Into Unfriendly Territory *1551L ; turn on drive motor manually 1551- BD 89 C0 LDA $C089,X ; initialize death counters 1554- A9 56 LDA #$56 1556- 85 FD STA $FD 1558- A9 08 LDA #$08 155A- C6 FC DEC $FC 155C- D0 04 BNE $1562 ; If the Death Counter hits zero, that ; would be bad. Which part of "Death ; Counter" sounded good to you, anyway? 155E- C6 FD DEC $FD 1560- F0 34 BEQ $1596 ; look for an $FB nibble 1562- BC 8C C0 LDY $C08C,X 1565- 10 FB BPL $1562 1567- C0 FB CPY #$FB 1569- D0 ED BNE $1558 156B- F0 00 BEQ $156D ; 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) 156D- EA NOP 156E- EA NOP ; read data latch (note: no BPL loop ; here, we're just reading it once) 156F- 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) 1572- C0 08 CPY #$08 ; rotate the carry into the low bit of ; the accumulator 1574- 2A ROL ; if we just rolled a "1" bit out of ; the high bit of the accumulator, take ; this branch 1575- B0 0B BCS $1582 ; next nibble needs to be $FF 1577- BC 8C C0 LDY $C08C,X 157A- 10 FB BPL $1577 ; ...otherwise we start over 157C- C0 FF CPY #$FF 157E- D0 D8 BNE $1558 ; loop back to get next nibble 1580- F0 EB BEQ $156D ; execution continues here (from $1575) ; get another nibble 1582- BC 8C C0 LDY $C08C,X 1585- 10 FB BPL $1582 ; stash it in zero page 1587- 84 FC STY $FC ; if the accumulator is anything but ; %00001010, start over 1589- C9 0A CMP #$0A 158B- D0 CB BNE $1558 ; get a nibble 158D- BD 8C C0 LDA $C08C,X 1590- 10 FB BPL $158D ; more bit twiddling 1592- 38 SEC 1593- 2A ROL ; AND it with the previously stashed ; nibble 1594- 25 FC AND $FC 1596- 49 AA EOR #$AA ; ...and store it in $15C0. That isn't ; used anywhere in this routine. Note: ; if the nibble check fails (because ; zero page $FD hits 0), the code does ; not error out or immediately reboot. ; It just branches to here (technically ; one instruction above) and continues. ; But $15C0 will have the wrong value. 1598- 8D C0 15 STA $15C0 ; "This nibble check will self-destruct ; in ten seconds..." 159B- A9 00 LDA #$00 159D- A8 TAY 159E- 99 41 15 STA $1541,Y 15A1- C8 INY 15A2- C0 5D CPY #$5D 15A4- D0 F8 BNE $159E 15A6- 60 RTS And that's it. Whether the nibble check succeeds or fails, this routine returns to the caller. It doesn't even set a flag. (Lots of nibble checks clear the carry flag if they succeed and set it if they fail, following the convention of the DOS 3.3 RWTS routines.) The only difference is the value of $15C0. Continuing after the JSR to $1551... *1538L ; pop $C5/$FF off the stack 1538- 68 PLA 1539- 68 PLA ; continue execution 153A- 4C A7 15 JMP $15A7 So that's the success path: $1541 needs to successfully call the RWTS to position the drive head, then $1551 performs the nibble check and sets the value of $15C0 and returns regardless, then execution continues at $15A7. *15A7L ; a memory move 15A7- A9 0F LDA #$0F 15A9- 85 FA STA $FA 15AB- A9 15 LDA #$15 15AD- 85 FB STA $FB 15AF- A9 A7 LDA #$A7 15B1- 85 FC STA $FC 15B3- A9 15 LDA #$15 15B5- 85 FD STA $FD 15B7- 20 EA 16 JSR $16EA ; another memory move 15BA- 20 16 17 JSR $1716 15BD- 4C CF 15 JMP $15CF *15CFL 15CF- AD CC 15 LDA $15CC 15D2- 18 CLC 15D3- 6D C0 15 ADC $15C0 15D6- 85 02 STA $02 15D8- AD CD 15 LDA $15CD 15DB- 38 SEC 15DC- ED C0 15 SBC $15C0 15DF- 85 03 STA $03 15E1- AD CE 15 LDA $15CE 15E4- 4D C0 15 EOR $15C0 15E7- 85 04 STA $04 There it is: three operations that rely on the correct value in $15C0. Luckily, this code is not obfuscated or even difficult to patch. I can do it right now without rebooting. *153A:4C 59 FF *1300G *15C0 15C0- 55 The correct value is $55. Now I can hard-code that value on disk. [Disk Fixer] --> "D" for directory mode --> select file "PM2* THE" --> arrows to follow the binary T12,S03,$C4 change "00" to "55" And change the initial jump at $1300 to bypass all this tomfoolery and go straight to the real code at $15A7: T12,S05,$05 change "00" to "A7" Quod erat liberandum. ~ Changelog 2015-09-10 - Vastly improved explanation of the actual protection routine. Thanks to qkumba for pointing out that my original explanation was inaccurate. 2015-08-26 - initial release --------------------------------------- A 4am crack No. 227 ------------------EOF------------------