-----------------Zork I---------------- A 4am crack 2018-03-19 --------------------------------------- Name: Zork I Version: release 5 Genre: adventure Year: 1980 Credits: Infocom Publisher: Personal Software, Inc. Platform: Apple ][ or later (32K) Media: 5.25-inch disk Sides: 1 OS: custom Previous cracks: none ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways Because The Steady March of Progress Is A Real Kick In The Pants First things first: this is a 13-sector disk. As in, it will not even boot on "modern" 16-sector floppy drives -- the ones Apple introduced in 1980. There was a transition period in the early 1980s where software developers put two bootloaders on track 0, one for the old 13-sector drives and another for the "new" 16-sector drives. This disk does not have that. It's 13 sectors or bust. If you have a "new" 16-sector drive, you can boot the game by booting the DOS 3.3 System Master disk and typing ]BRUN BOOT13 then putting the original disk in the drive. All my automated tools are useless. Passport can trace 13-sector disks -- but only if it contains the 16-sector shim bootloader. COPYA can only copy 16-sector disks. Same with Locksmith Fast Disk Backup. Even my favorite sector editor, Disk Fixer, has no capability to read 13-sector disks. Time to get some new (old) tools. Next steps: 1. Find tools that can read and/or copy 13-sector disks 2. ??? ~ Chapter 1 In Which We Find Some New Old Tools Not everything I have is useless, but I get to start using them differently. Without straying too far from the familiar, I started with Copy II Plus. Version 5.5 was the last version to support 13-sector disks in the main utilities. (Later versions could still bit copy them, of course.) Selecting "NEW DISK INFO" and changing Slot 6, Drive 1 from "DOS 3.3" to "DOS 3.2" will tell Copy II Plus that there is a 13-sector disk in that drive. Selecting "VERIFY DISK" successfully reads and verifies track 0 of the original disk, then starts giving read errors on every sector of every track. Then, surprisingly, verifies track $1D through $22 without complaint. Perhaps those higher tracks are unused? --v-- VERIFY DISK DISK A ERROR TRACK $17 SECTOR $0 1 2 3 4 5 6 7 8 9 A B C ERROR TRACK $18 SECTOR $0 1 2 3 4 5 6 7 8 9 A B C ERROR TRACK $19 SECTOR $0 1 2 3 4 5 6 7 8 9 A B C ERROR TRACK $1A SECTOR $0 1 2 3 4 5 6 7 8 9 A B C ERROR TRACK $1B SECTOR $0 1 2 3 4 5 6 7 8 9 A B C ERROR TRACK $1C SECTOR $0 1 2 3 4 5 6 7 8 9 A B C TOTAL : 364 ERRORS PRESS [RETURN] --^-- OK, so tracks $01-$1C are protected. If track $00 is read by the drive firmware and tracks $1D-$22 are unused, then I'm dealing with one structural protection covering all the game assets. Copy II Plus also has a sector editor. In version 5.5, it's part of the main utilities. (Later versions bumped it to the bit copy utility.) If I can verify track $00, maybe I can inspect it too? Indeed, the sector editor gets me in the front door. I can read individual sectors; I can list disassembled code. Here is the code on track 0, sector 1: --v-- ; RWTS call? 0900- A9 23 LDA #$23 0902- A0 C0 LDY #$C0 0904- 20 00 29 JSR $2900 ; try forever until success 0907- B0 F7 BCS $0900 ; increment address and sector 0909- EE C9 23 INC $23C9 090C- EE C5 23 INC $23C5 ; 13 sectors per track 090F- AD C5 23 LDA $23C5 0912- C9 0D CMP #$0D 0914- D0 EA BNE $0900 ; reset sector and increment track 0916- A9 00 LDA #$00 0918- 8D C5 23 STA $23C5 091B- EE C4 23 INC $23C4 ; up to (but not including) track 3 091E- AD C4 23 LDA $23C4 0921- C9 03 CMP #$03 0923- D0 DB BNE $0900 ; set some zero page addresses (not ; sure what these mean yet) 0925- A9 60 LDA #$60 0927- 85 7C STA $7C 0929- A9 0D LDA #$0D 092B- 85 7F STA $7F 092D- A9 1F LDA #$1F 092F- 85 7B STA $7B ; initialize text and input vectors 0931- 20 2F FB JSR $FB2F 0934- 20 93 FE JSR $FE93 0937- 20 89 FE JSR $FE89 ; jump to code we just read 093A- 4C 00 08 JMP $0800 -^-- This sector editor always disassembles code as if it started at $0900, but I'm fairly sure this code is really loaded at $2300. The first three instructions appear to be calling a DOS-shaped RWTS entry point at $2900, and there is an RWTS parameter table at offset $C0 of this sector, the address of which it is passing in A and Y. --v-- C0- 01 60 01 00 01 00 D1 23 .`....Q# ^^^^^ track 1, sector 0 C8- 00 08 00 00 01 00 00 60 .......` ^^^^^ address $0800 D0- 01 00 01 EF D8 00 00 00 ...oX... --^-- So we're reading all of track 1 and 2 into $0800+, which, at 13 sectors per track, works out to $0800..$20FF. Further investigation with Copy II Plus bit copy confirms that it can produce a working copy on another floppy disk. Whatever protection is preventing the main Copy II Plus utilities from verifying tracks $01-$1C, it doesn't fool the bit copier. This is excellent news. The combination of a working copy and a sector editor that can write to that working copy, means I can remove the original disk from the equation and start hacking up this code directly. ~ Chapter 2 In Which We Hack Up This Code Directly The first change I want to make is to break into the monitor instead of executing this code. Thus -- ON A COPY, NOT THE ORIGINAL THAT SELLS FOR $1200 ON EBAY -- I used the Copy II Plus 5.5 sector editor to make this small patch: T00,S01,$00: A923A0 -> 4C59FF Booting my hacked copy (through BOOT13, like the original disk) successfully drops me into the monitor with the boot code and protected RWTS in memory. *2300L 2300- 4C 59 FF JMP $FF59 *2900L 2900- 84 48 STY $48 2902- 85 49 STA $49 2904- A0 02 LDY #$02 2906- 8C F8 06 STY $06F8 2909- A0 04 LDY #$04 290B- 8C F8 04 STY $04F8 ...and so forth. I believe this RWTS is capable of reading the protected tracks $01-$1C. Let's save it to a file and find out. [S5,D1=my work disk] *C500G ... ]BSAVE OBJ.2200-2BFF,A$200,L$A00 ~ Chapter 3 In Which We Attempt To Use The Disk As A Weapon Against Itself Advanced Demuffin is a cracker's tool to convert disks to a standard format. It takes a copy of the original disk's RWTS (which you must supply), uses that to read the original, while using its own copy of a standard RWTS to write out a copy in a standard format, sector by sector. I've included the latest version of Advanced Demuffin on my work disk. ]BLOAD ADVANCED DEMUFFIN 1.5 By a fortuitous coincidence, Advanced Demuffin and this protected RWTS do not overlap each other in memory. But I do need to make one small adjustment: by default, Advanced Demuffin will store sector data at $2000-$8FFF, which would obliterate the protected RWTS at $2400 and crash after reading a few sectors. Thus: ]CALL -151 *1CF0:40 ; save sector data at $4000 ; (instead of $2000) See the Advanced Demuffin documentation for details on runtime parameters. https:// archive.org/details/AdvancedDemuffin15 With that tweak in place, I can start the conversion: *800G ; launch Advanced Demuffin [press "C" to convert disk] ["Y" to change default values] --v-- ADVANCED DEMUFFIN 1.5 (C) 1983, 2014 ORIGINAL BY THE STACK UPDATES BY 4AM ======================================= INPUT ALL VALUES IN HEX SECTORS PER TRACK? (13/16) 13 <-- START TRACK: $01 <-- START SECTOR: $00 END TRACK: $1C <-- END SECTOR: $0C <-- INCREMENT: 1 MAX # OF RETRIES: 0 COPY FROM DRIVE 1 TO DRIVE: 2 ======================================= 13SC $01,$00-$1C,$0C BY1.0 S6,D1->S6,D2 --^-- [S6,D1=original disk] [S6,D2=blank disk] And here we go... --v-- ADVANCED DEMUFFIN 1.5 (C) 1983, 2014 ORIGINAL BY THE STACK UPDATES BY 4AM =======PRESS ANY KEY TO CONTINUE======= TRK: ............................ +.5: 0123456789ABCDEF0123456789ABCDEF012 SC0: ............................ SC1: ............................ SC2: ............................ SC3: ............................ SC4: ............................ SC5: ............................ SC6: ............................ SC7: ............................ SC8: ............................ SC9: ............................ SCA: ............................ SCB: ............................ SCC: ............................ SCD: SCE: SCF: ======================================= 13SC $01,$00-$1C,$0C BY1.0 S6,D1->S6,D2 --^-- This is the power and the genius of Advanced Demuffin. Every disk must be able to read itself. So, let it read itself, then capture the data and write it out in a standard format. Now what? ~ Chapter 4 Franken-Zork I have, or think I have, all the game code and data on tracks $01-$1C of an unprotected 16-sector disk. (The last three sectors of each track are blank.) This is not the most convenient format, but it's progress. The only thing I'm missing is track 0. I can't simply copy the code from the original disk, because the RWTS is for a 13-sector disk. It's not a matter of patching some prologue or epilogue values. The entire process of decoding disk nibbles to memory bytes is radically different. Although... DOS 3.2 (13-sector) and DOS 3.3 (16- sector) are remarkably similar, on purpose. Pretty much the only thing that changed between the last version of 3.2 and the first version of 3.3 was the RWTS. Furthermore, Apple kept the entry points and calling convention the same. You can create a "Franken-disk" that contains the DOS 3.2 OS code but the DOS 3.3 RWTS. (Passport does this to auto-convert protected 13-sector disks.) Infocom released later versions of Zork as protected 16-sector disks. If they, like Apple, swapped in 16-sector RWTS code but kept the calling convention the same, maybe I can do the same? I have previously cracked Zork I r15 (crack #1459), which I believe was the first version of Zork I that Infocom released under their own name. Let's take a look at that disk's bootloader. [S6,D1=Zork I r15] [back to my favorite sector editor, because there ain't no memory like muscle memory] Track 0, sector 0 looks like a DOS 3.3 boot sector, but it loads at $2200 instead of $B600. That's promising! Sector 1 looks like this: --v-- T00,S01 ----------- DISASSEMBLY MODE ---------- 0000:A9 1F LDA #$1F 0002:85 7B STA $7B ; call RWTS 0004:A9 23 LDA #$23 0006:A0 C0 LDY #$C0 0008:20 00 29 JSR $2900 ; try forever until success 000B:B0 F7 BCS $0004 ; increment address 000D:EE 43 23 INC $2343 ; until a sector counter 0010:AD 43 23 LDA $2343 0013:C9 1A CMP #$1A 0015:F0 18 BEQ $002F ; increment address and sector 0017:EE C9 23 INC $23C9 001A:EE C5 23 INC $23C5 ; 16 sectors per track 001D:AD C5 23 LDA $23C5 0020:C9 10 CMP #$10 0022:D0 E0 BNE $0004 ; reset sector and increment track 0024:A9 00 LDA #$00 0026:8D C5 23 STA $23C5 0029:EE C4 23 INC $23C4 002C:4C 04 23 JMP $2304 ; Execution continues here from the BEQ ; at $2315, once the sector counter ; hits $1A. ; Set some more zero page addresses 002F:A9 60 LDA #$60 0031:85 7C STA $7C ; This, in particular, was #$0D on the ; original 13-sector disk. Perhaps they ; built in the number of sectors per ; track into their interpreter? Could I ; really be that lucky? 0033:A9 10 LDA #$10 0035:85 7F STA $7F ; same machine stuff 0037:20 2F FB JSR $FB2F 003A:20 93 FE JSR $FE93 003D:20 89 FE JSR $FE89 ; and jump to the code we just read 0040:4C 00 08 JMP $0800 --^-- Other than using a counter to control how many sectors to read, this bootloader is almost identical to the original 13-sector version. (The original 13-sector disk read all of tracks $01 and $02.) More importantly, it appears that Infocom did do exactly what I had hoped they would do: swap out their 13-sector RWTS with a 16- sector RWTS in exactly the same memory range, $2400..$2BFF. Which means it's time for me to make a Franken-Zork. [S6,D1=Zork r15 (16-sector bootloader)] [S6,D2=my work-in-progress Zork r5] [Copy II Plus] [MANUAL SECTOR COPY] [Source: Slot 6, Drive 1] [Target: Slot 6, Drive 2] [Tracks: 0 only] [GO] Great. Now I have the r5 game code on tracks $01-$1C (sectors $00-$0C of each track), the r15 bootloader on track 0, and no idea if it's going to work. Ah, wait. Because the code on tracks $01-$02 is only stored on the first 13 sectors of each track, I actually want the original code on track 0, sector 1. Except for zero page $7F, which (if my hunch is correct), is a parameter to tell the Infocom interpreter that the disk has 13 sectors of usable data on each track. Thus, my final boot code on T00,S01 looks exactly like the original 13- sector disk. I'd like to tell you I had some fancy way of transferring it, but in reality I typed it out by hand and made a stupid typo which I will elide over for the purposes of this write-up. Franken-Zork indeed. ]PR#6 ...crashes at $1DF1... Well, foo. ~ Chapter 5 Franken-Zork Strikes Back Investigating in the monitor at the point of the crash, it looks like at least some of the code from tracks $01- $02 is being loaded. Which is great. $0800, for instance, looks like this: 1DF1- A=2B X=00 Y=7F P=31 S=F9 *800L 0800- D8 CLD 0801- A9 00 LDA #$00 0803- A2 80 LDX #$80 0805- 95 00 STA $00,X 0807- E8 INX 0808- D0 FB BNE $0805 080A- A2 FF LDX #$FF 080C- 9A TXS ...which is stored on T01,S00. So we're definitely loading interpreter from tracks $01-$02. $0900..$09FF, however, is all zeroes. Which is not great. $0A00 has code, but it's the wrong code (or rather, code that belongs somewhere else): *A00L 0A00- 4C A7 09 JMP $09A7 0A03- 20 09 1D JSR $1D09 0A06- 20 97 15 JSR $1597 0A09- 18 CLC 0A0A- A5 82 LDA $82 0A0C- 65 BA ADC $BA 0A0E- 85 82 STA $82 Returning to my trusty sector editor, that code is from T01,S0B. It should have been loaded into $1300..$13FF. Aha! This is a sector ordering problem. Looking at the RWTS I copied from the 16-sector Zork r15, I confirm this suspicion: --v-- -------------- DISK EDIT -------------- TRACK $00/SECTOR $09/VOLUME $FE/BYTE$B8 --------------------------------------- $A8: FF FF FF FF FF FF FF FF ........ $B0: FF FF FF FF FF FF FF FF ........ $B8:>00<04 08 0C 01 05 09 0D @DHLAEIM $C0: 02 06 0A 0E 03 07 0B 0F BFJNCGKO $C8: 20 93 FE AD 81 C0 AD 81 .~-.@-. $D0: C0 A9 00 8D 00 E0 4C 44 @)@.@`LD --^-- The 16-byte array at offset $B8 is the mapping between physical and logical sectors. Standard 16-sector disks, including my work-in-progress Franken- Zork, use a different mapping: --v-- -------------- DISK EDIT -------------- TRACK $00/SECTOR $09/VOLUME $FE/BYTE$B8 --------------------------------------- $B8:>00<0D 0B 09 07 05 03 01 @MKIGECA $C0: 0E 0C 0A 08 06 04 02 0F NLJHFDBO --^-- I made that change to my work-in- progress Franken-Zork and rebooted. T00,S09,$B8: 0004080C0105090D02060A0E03070B0F -> 000D0B09070503010E0C0A080604020F ]PR#6 ...boots just as far, but hangs instead of crashing... I am not at all sure this is progress. ~ Chapter 6 In Which Optimizations Have A Way Of Coming Back To Bite You 38 Years Later Pressing gets me to the monitor. Checking $0800+, it appears that the sector map is now correct. The code stored on T01,S00 is at $0800; the code on T01,S01 is at $0900; and so on. So that is definitely progress. So what's going on now? To answer that question, I delved into the interpreter code itself, in memory at $0800. The relevant code starts at $086A: *86AL 086A- A9 00 LDA #$00 086C- 85 BE STA $BE 086E- A9 7F LDA #$7F 0870- 85 BF STA $BF 0872- A9 00 LDA #$00 0874- 85 BA STA $BA 0876- A9 2B LDA #$2B 0878- 85 BB STA $BB ; sets some zero page addresses and ; clears the screen via $FC58 087A- 20 68 1E JSR $1E68 087D- A5 BA LDA $BA 087F- 85 E4 STA $E4 0881- A5 BB LDA $BB 0883- 85 E5 STA $E5 0885- A9 00 LDA #$00 0887- 85 E2 STA $E2 0889- A9 00 LDA #$00 088B- 85 E3 STA $E3 088D- 20 34 1E JSR $1E34 0890- 90 03 BCC $0895 *1E34L 1E34- A9 01 LDA #$01 1E36- 20 F3 1D JSR $1DF3 *1DF3L ; This appears to be part of an RWTS ; parameter table starting at $1DDA. ; That would mean $1DEA is the RWTS ; command. $01=read 1DF3- 8D EA 1D STA $1DEA ; Zero page $E4 was set from zero page ; $BA (at $087F) which was set to #$00 ; (at $0874), and it's being stored in ; the RWTS parameter table as the low ; byte of the memory address to store ; the sector data we're about to read. 1DF6- A5 E4 LDA $E4 1DF8- 8D E6 1D STA $1DE6 ; Zero page $E5 was set from zero page ; $BB (at $0883) which was set to #$2B ; (at $0878), and it's being used as ; the high byte of the read address. 1DFB- A5 E5 LDA $E5 1DFD- 8D E7 1D STA $1DE7 ; Starting at track $03, this takes a ; block number in zero page $E2 and ; calculates the actual track/sector ; based on the number of sectors per ; track (in zero page $7F, as I ; correctly guessed earlier). 1E00- A9 03 LDA #$03 1E02- 8D E2 1D STA $1DE2 1E05- A5 E2 LDA $E2 1E07- A6 E3 LDX $E3 1E09- 38 SEC 1E0A- E5 7F SBC $7F 1E0C- B0 04 BCS $1E12 1E0E- CA DEX 1E0F- 30 07 BMI $1E18 1E11- 38 SEC 1E12- EE E2 1D INC $1DE2 1E15- 4C 0A 1E JMP $1E0A 1E18- 18 CLC 1E19- 65 7F ADC $7F 1E1B- 8D E3 1D STA $1DE3 ; call the RWTS 1E1E- A9 1D LDA #$1D 1E20- A0 DE LDY #$DE 1E22- 20 00 29 JSR $2900 1E25- 60 RTS So what's the problem? The very first disk read is into $2B00 -- the last page of the RWTS itself. How did this original disk ever work? Well, funny story about that. It turns out that, in a 13-sector RWTS, the last page of code is only used to format a disk. Since Infocom games never need to format a disk, and memory was super tight, they decided to reuse the unused last page of the RWTS for game data. In a 16-sector RWTS, the last page of code is only used to format a disk... and to hold the sector mapping array (that 16-byte table that I just patched in the previous chapter). Memory was so tight in 1980 that they literally OVERWROTE THEIR OWN LOW-LEVEL DISK DRIVER but like just a little bit. Just the part they weren't using. But... I need that page. But do I? Seriously, the *only* thing in that sector that I'm using is the 16-byte sector order array. I suppose I could move that somewhere else and let this interpreter continue to OVERWRITE ITS OWN LOW-LEVEL DISK DRIVER which is kind of a ridiculous thing to do in 2018 but I guess this is all a little ridiculous in 2018 so okay let's do this. Early versions of DOS 3.3 had some unused memory at $BA69. (This was later used for OS-level patches.) Looking at T00,S04, Infocom was not using this for anything. I can easily fit the 16-byte sector ordering table there. T00,S04,$69: -> 000D0B09070503010E0C0A080604020F The code that uses the sector ordering table it is on T00,S08. Since this RWTS lives at $2400 instead of $B800, the old address is $2BB8 and the new address is $2669: T00,S08,$2C: B82B -> 6926 ]PR#6 ...works, and it is glorious... (Brief epilogue: how did the 16-sector releases of Zork handle this problem? It turns out that Infocom's interpreter centrally manages memory allocation, so they could simply load the first page of game data into $2C00 instead of $2B00. No other changes required. I considered doing this instead of moving the sector ordering table, but in the end I decided that it was cleaner to keep all patches within the RWTS and leave the interpreter code untouched.) Quod erat liberandum. ~ Acknowledgments Thanks to Ian Baronofsky for lending me original disk. Thanks to @brouhaha on Twitter for helping me understand the interpreter enough to patch it. Thanks to qkumba for proposing a solution that didn't involve patching the interpreter after all. --------------------------------------- A 4am crack No. 1724 ------------------EOF------------------