----------The Hangtown Trilogy--------- A 4am crack 2016-04-27 --------------------------------------- Name: The Hangtown Trilogy Version: 2.2 Genre: adventure Year: 1990 Credits: Designed by John Doolittle Programmed by Eric Thornton Publisher: Critical Thinking Press Media: double-sided 5.25-inch floppy OS: ProDOS 1.0.1 Previous cracks: none Similar cracks: #657 Five in a Row: Addition and Subtraction ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways COPYA no errors, but copy loads ProDOS then reboots with the drive motor still on 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 runtime protection check in the startup program. (Disks do not spontaneously reboot unless someone tells them to.) Next steps: 1. Trace the startup program 2. Find the protection check and disable it 3. Declare victory(*) (*) take a nap ~ Chapter 1 In Which We Run Into An Old Friend And Begin To Question Our Taste In Friends [S7,D1=my ProDOS hard drive] [S6,D1=original disk (side A)] ]PR#7 ... ]CAT,S6,D1 /HT NAME TYPE BLOCKS MODIFIED *PRODOS SYS 31 1-JAN-84 *RT.MAIN.OBJ1 BIN 11 3-NOV-87 *RT.AUX.OBJ1 BIN 14 20-OCT-87 RT.AUX.OBJ0 BIN 13 7-JUN-90 HANGDATA TXT 10 26-SEP-90 FARMDATA TXT 9 24-SEP-90 PRATDATA TXT 9 24-SEP-90 FARMTOWN.OBJ $F5 44 24-SEP-90 MENU.LNK BIN 13 24-SEP-90 STARTUP.SYSTEM SYS 18 24-SEP-90 HANGTOWN.OBJ BIN 49 24-SEP-90 PRATTOWN.OBJ BIN 41 24-SEP-90 BLOCKS FREE: 203 BLOCKS USED: 77 ]PREFIX /HT ]BLOAD STARTUP.SYSTEM,A$2000,TSYS ]CALL-151 *2000L ; save some zero page locations 2000- A5 F0 LDA $F0 2002- 8D 0C 21 STA $210C 2005- A5 F1 LDA $F1 2007- 8D 0D 21 STA $210D ; fill in an MLI parameter table 200A- A9 03 LDA #$03 200C- 8D 42 20 STA $2042 ; get current slot (x16) 200F- AD 30 BF LDA $BF30 2012- 8D 43 20 STA $2043 2015- A9 1C LDA #$1C 2017- 8D 44 20 STA $2044 201A- A9 4C LDA #$4C 201C- 8D 45 20 STA $2045 201F- A9 01 LDA #$01 2021- 8D 46 20 STA $2046 2024- A9 00 LDA #$00 2026- 8D 47 20 STA $2047 ; raw block read (MLI command $80) 2029- 20 00 BF JSR $BF00 202C- [80 42 20] The MLI parameter table for block reads takes three parameters (set at $200C). The first is the slot number x16 (set at $2012). The second is the target address ($4C1C, set at $2015). The third is the block number ($0001, set at $201F). It doesn't check the error code, so I guess we only cared about moving the drive head to a specific track. ; now turn on drive motor manually ; (always suspicious) 202F- AE 30 BF LDX $BF30 2032- BD 89 C0 LDA $C089,X ; looks like a Death Counter 2035- A9 0B LDA #$0B 2037- 8D 0B 21 STA $210B 203A- D0 1F BNE $205B ; Once Death Counter hits 0, we fall ; through to here, which resets the ; computer. (It never turns off the ; drive motor.) 203C- EE F4 03 INC $03F4 203F- 6C FC FF JMP ($FFFC) This explains the behavior I saw on my non-working copy. *205BL ; decrement first Death Counter and ; give up if it hits 0 205B- CE 0B 21 DEC $210B 205E- F0 DC BEQ $203C ; set up another Death Counter for the ; next phase 2060- A9 21 LDA #$21 2062- 85 F0 STA $F0 2064- C6 F0 DEC $F0 2066- F0 F3 BEQ $205B ; get next address field (not shown) 2068- 20 C3 20 JSR $20C3 206B- B0 EE BCS $205B ; check physical sector (from address ; field) 206D- A5 2D LDA $2D ; if not $0B, try again 206F- C9 0B CMP #$0B 2071- D0 F1 BNE $2064 2073- A0 28 LDY #$28 2075- A9 D5 LDA #$D5 2077- 20 4B 20 JSR $204B ; find nibble A (=$D5) within the next ; Y nibbles (=$28) 204B- 85 F1 STA $F1 204D- BD 8C C0 LDA $C08C,X 2050- 10 FB BPL $204D 2052- C5 F1 CMP $F1 2054- F0 F2 BEQ $2048 ; RTS 2056- 88 DEY 2057- D0 F4 BNE $204D 2059- 68 PLA 205A- 68 PLA Then it falls through to $205B, the same entry point we called in the first place. (It decrements the first Death Counter and starts over.) So this functions as a callable subroutine unless something goes wrong, in which case it functions as a loop. Wild. Continuing from $207A... 207A- A0 FF LDY #$FF 207C- 20 49 20 JSR $2049 *2049L 2049- A9 E7 LDA #$E7 (Then it falls through to $204B, which looks for a nibble in the time allotted by the Y register, returning gracefully if it finds the nibble or decrementing decrements the counter and starting over from $205B. So this call looks for an $E7 nibble within the next $FF nibbles and returns if it finds it, or pops the return address and starts the loop over if it doesn't find it. Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaagh. I would scream longer, but I'm limited by column length.) Continuing from $207F... ; find another $E7 nibble within the ; next 2 nibbles 207F- A0 02 LDY #$02 2081- 20 49 20 JSR $2049 ; find another $E7 nibble immediately 2084- BD 8C C0 LDA $C08C,X 2087- 10 FB BPL $2084 2089- C9 E7 CMP #$E7 208B- D0 CE BNE $205B ; reset data latch 208D- BD 8D C0 LDA $C08D,X ; waste some time 2090- A0 10 LDY #$10 2092- 24 80 BIT $80 ; find an $EE nibble within the next ; $10 nibbles (Y reset to $10 at $2090) 2094- A9 EE LDA #$EE 2096- 20 4B 20 JSR $204B ; store the next 8 nibbles in memory ; (backwards) 2099- A0 07 LDY #$07 209B- BD 8C C0 LDA $C08C,X 209E- 10 FB BPL $209B 20A0- 99 F8 21 STA $21F8,Y 20A3- 88 DEY 20A4- 10 F5 BPL $209B ; turn off drive motor 20A6- BD 88 C0 LDA $C088,X ; copy some code to higher memory 20A9- A0 1B LDY #$1B 20AB- B9 0E 21 LDA $210E,Y 20AE- 99 00 4C STA $4C00,Y 20B1- 88 DEY 20B2- 10 F7 BPL $20AB 20B4- AD 0C 21 LDA $210C 20B7- 85 F0 STA $F0 20B9- AD 0D 21 LDA $210D 20BC- 85 F1 STA $F1 20BE- A0 00 LDY #$00 ; and continue there 20C0- 4C 00 4C JMP $4C00 *20C0:60 *20A9G *4C00L ; use the 8 nibbles we stored earlier ; as a 64-bit decryption key for the ; rest of this file 4C00- 98 TYA 4C01- 29 07 AND #$07 4C03- AA TAX 4C04- 98 TYA 4C05- 5D F8 21 EOR $21F8,X 4C08- 48 PHA 4C09- 59 1C 4C EOR $4C1C,Y 4C0C- 99 00 20 STA $2000,Y 4C0F- 68 PLA 4C10- 59 1C 4D EOR $4D1C,Y 4C13- 99 00 21 STA $2100,Y 4C16- C8 INY 4C17- D0 E7 BNE $4C00 ; and continue with decrypted code 4C19- 4C 00 20 JMP $2000 Someone spent a bit of time refactoring this, but the logic remains the same: this checks for a series of $E7 nibbles before intentionally desynchronizing and looking for nibbles "out of phase." The loop at $2099 stores the decryption key at $21F8, which is part of this program space. That means I could hard- code the correct values in the program itself and skip to the decryption loop at $20A9. But that's not what we're going to do. We're going to do one better. ~ Chapter 2 In Which We Take A Short Digression Into Some Theory So We Can Better Understand The Upcoming Bombshell $E7 $E7 $E7 $E7. What would that nibble sequence look like on disk? The answer is, "It depends." $E7 in hexadecimal is 11100111 in binary, so here is the simplest possible answer: |--E7--||--E7--||--E7--||--E7--| 11100111111001111110011111100111 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.) The standard "wait for data latch to have its high bit set" loop, which you see over and over in any RWTS code, "swallows" these extra "0" bits between the nibbles: :1 LDA $C08C,X BPL :1 Now consider the following bitstream: |--E7--| |--E7--| |--E7--||--E7--| 11100111011100111001110011111100111 ^ ^^ (extra) (extra) The first $E7 has one extra "0" bit after it, and the second $E7 has two extra "0" bits after it. Totally legal, works on any Apple II computer and any floppy drive. A "LDA $C08C,X; BPL" loop would still interpret this bitstream as a sequence of four $E7 nibbles. Each of the extra "0" bits appear after we've just read a nibble and we're waiting for the most significant bit to go high again. So they get swallowed, ignored, like they were never there. But what if we miss some of this bitstream, then start looking? The disk is always spinning, whether we're reading from it or not. If we waste too much time doing something other than reading, we'll miss bits as the disk spins. (This is why the timing of low-level RWTS code is so critical.) Let's say we waste 12 CPU cycles before we start reading this bitstream. Each bit takes 4 CPU cycles to go by, so after 12 cycles, we would have missed the first 3 bits (marked with an X). (normal start) |--E7--| |--E7--| |--E7--||--E7--| 11100111011100111001110011111100111 XXX |--EE--| |--E7--| |--FC--| (delayed start) Ah! It's interpreted as a different nibble sequence if you delay even a handful of CPU cycles before you start reading. Also, some of those "extra" bits are no longer ignored; now they're interpreted as data, as part of the nibbles returned to the higher level code. Meanwhile, other bits that were part of the $E7 nibbles get "swallowed" instead. Now, let's go back to the first stream, which had no extra bits between the nibbles, and see what happens when we waste those same 12 CPU cycles. (normal start) |--E7--||--E7--||--E7--||--E7--| 11100111111001111110011111100111 XXX |--FC--||--FC--||--FC--| (delayed start) After skipping the first three bits, the RWTS interprets the bitstream as $FC $FC $FC, repeating endlessly -- not $EE $E7 $FC like the other stream. Here's the kicker: generic bit copiers didn't preserve these extra "0" bits between nibbles. Even top-of-the-line bit copiers couldn't reliably detect the difference between 1 timing bit and 2 timing bits. By "desynchronizing" (wasting just the right number of CPU cycles at just the right time), then interpreting the bits on the disk in mid-stream, developers could determine at runtime whether you had an original disk. Here is the complete "E7 bitstream," annotated to show both the synchronized and desynchronized nibble sequences. |--E7--| |--E7--| |--E7--||--E7--| 111001110111001110011100111111001110 XXX |--EE--| |--E7--| |--FC--||--E |--E7--| |--E7--||--E7--| |--E7--| 111001110011100111111001110111001110 E--| |--E7--| |--FC--||--EE--| |--E |--E7--||--E7--| 1110011111100111 E--| |--FC--| ~ Chapter 3 It's Just A Phase You're Going Through This protection scheme hinges on the assumption that only an original disk will present the proper sequence of nibbles after the code intentionally "desynchronizes" mid-stream. This seems like an entirely reasonable assumption. After all, even the best bit copiers could not preserve the exact number of timing bits after every single nibble. However, that assumption rests on a deeper assumption. Once it burns 12 CPU cycles (skipping 3 bits and getting out of sync with the original nibble boundary), it assumes that the next nibbles it reads (EE E7 FC EE E7 FC EE EE FC) are dependent on the timing bits that were originally between the $E7 nibbles. In other words, it assumes that once it gets out of sync, it stays out of sync. But what if that weren't true? What if we could resynchronize the bitstream to the original nibble boundary -- after the code burned time intentionally to get out of sync? Imagine a sequence of nibbles which, when read by this code, would swallow an additional 5 bits and get back in sync with the original nibble boundary. This would need to happen before the code found the $EE nibble, because immediately after that, it starts reading additional nibbles and checking them against a hard-coded array. But there is a small window there, after we desynchronize but before we find the $EE nibble. This is the relevant code: 208D- BD 8D C0 LDA $C08D,X 2090- A0 10 LDY #$10 2092- 24 80 BIT $80 2094- A9 EE LDA #$EE 2096- 20 4B 20 JSR $204B The Y register gets reset to $10 (at $2090) then decremented while we're looking for the $EE nibble. That means we can skip up to 15 nibbles between the third $E7 and $EE, and execution will still continue without branching to the failure path. Now watch this: (normal start) |--EF--||--F3--||--FC--||--EE--| 11101111111100111111110011101110 XXX |--FF--| |--FF--| |--EE--| (delayed start) If we put this bitstream immediately after the initial $E7 $E7 $E7 sequence, the call at $087E that's looking for the desynchronized $EE nibble will instead find two desynchronized $FF nibbles, skip over them (decrementing the Y register, but not all the way to zero), then finally find the $EE nibble and be happy. But look what happened in the meantime: we've resynchronized the bitstream to the original nibble boundary! By putting "0" bits before and after each desynchronized $FF nibbles, we've essentially "swallowed" the extra bits and shifted the phase from +3 to +8. Since nibbles are 8-bit values, a phase of +8 is the same as 0. In just 24 bits, we've resynchronized the bitstream and fooled the protection check into reading regular nibbles as if they're desynchronized nibbles. If we put this nibble sequence in the right place on our non-working copy, we don't need to bypass the protection code at all. The code can run as usual. Let it run. Let it search for nibbles. Let it desynchronize. Let it decrypt the code with magic nibble stream. It'll all work. We've defeated the E7 E7 E7 protection check with no code changes. ~ Chapter 4 In Which We Remove All Traces Of Copy Protection Using An Automated Tool That I Wrote For Just Such An Occasion [S6,D1=demuffin'd copy] [S5,D1=my work disk] ]PR#5 ... ]BRUN PDP T00,S02,$A3 change 00AC00AC00AC00AC00AC00AC to 64B444802CDC18B4448044B4 Wait, what? ~ Chapter 5 Here's What The E7 bitstream is part of the data field of a real sector on disk. On this disk, it's on T00,S02. Here's what it looks like in the Copy ][+ nibble editor: --v-- COPY ][ PLUS BIT COPY PROGRAM 8.4 (C) 1982-9 CENTRAL POINT SOFTWARE, INC. ---------------------------------------- TRACK: 00 START: 1800 LENGTH: 3DFF 2BA8: FF FF FF D5 AA 96 FF FE ^^^^^^^^ ^^^^^ address prologue V=254 2BB0: AA AA AF AB FE FB DE AA ^^^^^ ^^^^^ ^^^^^ ^^^^^ T=$00 S=$02 chksm epilogue 2BB8: EB FF FE FF FF FF FF FF 2BC0: FF FF FF FF FF D5 AA AD ^^^^^^^^ data prologue 2BC8: 96 96 96 96 96 96 96 96 2BD0: 96 96 96 96 96 96 96 96 2BD8: 96 96 96 96 96 96 96 96 2BE0: 96 96 96 96 96 96 96 96 2BE8: 96 96 96 96 96 96 96 96 2BF0: 96 96 96 96 96 96 96 96 2BF8: 96 96 96 96 96 96 96 96 2C00: 96 96 96 96 96 96 96 96 2C08: 96 96 96 96 96 96 96 96 2C10: 96 96 96 96 96 96 96 96 2C18: 96 96 96 96 96 96 96 96 2C20: 96 96 96 96 96 96 96 96 2C28: 96 96 96 96 96 96 96 96 2C30: 96 96 96 96 96 96 96 96 2C38: 96 96 96 96 96 96 96 96 2C40: 96 96 96 96 96 96 96 96 2C48: 96 96 96 96 96 96 96 96 2C50: 96 96 96 96 96 96 96 96 2C58: 96 96 96 96 96 96 96 96 2C60: 96 96 96 96 96 96 96 96 2C68: 96 96 96 96 96 96 96 96 2C70: 96 96 96 96 96 96 96 96 2C78: 96 96 96 96 96 96 96 96 2C80: 96 96 96 96 96 96 96 96 2C88: 96 96 96 96 96 96 96 96 2C90: 96 96 96 96 96 96 96 96 2C98: 96 96 96 96 96 96 96 96 2CA0: 96 96 96 96 96 96 96 96 2CA8: 96 96 96 96 96 96 96 96 2CB0: 96 96 96 96 96 96 96 96 2CB8: 96 96 96 96 96 96 E7 E7 ; timing 2CC0: E7 E7 E7 E7 E7 E7 E7 E7 ; bits 2CC8: E7 E7 E7 E7 E7 E7 E7 E7 ; are in 2CD0: E7 E7 E7 E7 E7 E7 E7 E7 ; here 2CD8: E7 E7 E7 E7 E7 E7 E7 E7 2CE0: E7 E7 E7 E7 E7 E7 E7 E7 2CE8: E7 E7 E7 E7 E7 E7 E7 E7 2CF0: E7 E7 E7 E7 E7 E7 E7 E7 2CF8: E7 E7 E7 E7 E7 E7 E7 E7 2D00: E7 E7 E7 E7 E7 E7 E7 E7 2D08: E7 E7 E7 E7 E7 E7 E7 E7 2D10: E7 E7 E7 E7 E7 E7 E7 E7 2D18: E7 E7 E7 E7 E7 E7 96 DE ^^ 2D20: AA EB 9F F3 FC FF FF FF ^^^^^ data epilogue --------------------------------------- A TO ANALYZE DATA ESC TO QUIT ? FOR HELP SCREEN / CHANGE PARMS Q FOR NEXT TRACK SPACE TO RE-READ --^-- Remember how the protection check looks for a $D5 nibble (the JSR at $2077)? The one it finds is the first nibble of the data prologue, shown here at offset $2BC5. Remember how the protection check gives itself a $100 nibble window to find the first $E7 nibble after it finds a $D5? It's skipping over most of the data field -- a bunch of $96 nibbles. This is a real sector. The protection check never reads T00,S02 as sector data, but you can see it in your favorite sector editor if you want. It looks like this: --v-- -------------- DISK EDIT -------------- TRACK $00/SECTOR $02/VOLUME $FE/BYTE$00 --------------------------------------- $00: 00 00 00 00 00 00 00 00 ........ $08: 00 00 00 00 00 00 00 00 ........ $10: 00 00 00 00 00 00 00 00 ........ $18: 00 00 00 00 00 00 00 00 ........ $20: 00 00 00 00 00 00 00 00 ........ $28: 00 00 00 00 00 00 00 00 ........ $30: 00 00 00 00 00 00 00 00 ........ $38: 00 00 00 00 00 00 00 00 ........ $40: 00 00 00 00 00 00 00 00 ........ $48: 00 00 00 00 00 00 00 00 ........ $50: 00 00 00 00 00 00 00 00 ........ $58: 00 00 00 00 00 00 00 00 ........ $60: 00 00 00 00 00 00 00 00 ........ $68: 00 00 00 00 00 00 00 00 ........ $70: 00 00 00 00 00 00 00 00 ........ $78: 00 00 00 00 00 00 00 00 ........ $80: 00 00 00 00 00 00 00 00 ........ $88: 00 00 00 00 00 00 00 00 ........ $90: 00 00 00 00 00 00 00 00 ........ $98: 00 00 00 00 00 00 00 00 ........ $A0: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $A8: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $B0: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $B8: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $C0: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $C8: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $D0: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $D8: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $E0: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $E8: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $F0: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. $F8: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. --------------------------------------- BUFFER 0/SLOT 6/DRIVE 1/MASK OFF/NORMAL --------------------------------------- COMMAND : _ --^-- Those $00 bytes in memory get written to disk as $96 nibbles. No magic there, just the standard 6-and-2 encoding that DOS 3.3 uses to convert bytes in memory to nibbles on disk. Furthermore, that repeated pattern of "AC 00", starting at offset $A0, are the $E7 nibbles at the end of the data field (shown above in the Copy ][+ nibble ditor at offset $2CB6). My non-working copy looks identical, except it lacks the timing bits between the $E7 nibbles, so the protection check fails. But after the 12-byte patch that my Post-Demuffin Patcher script made... T00,S02,$A3 change 00AC00AC00AC00AC00AC00AC to 64B444802CDC18B4448044B4 ...the sector looks like this: --v-- ------------- DISK EDIT --------------- TRACK $00/SECTOR $02/VOLUME $FE/BYTE$00 --------------------------------------- $00: 00 00 00 00 00 00 00 00 ........ . . [unchanged] . $98: 00 00 00 00 00 00 00 00 ........ $A0: AC 00 AC 64 B4 44 80 2C ,.,d4D., $A8: DC 18 B4 44 80 44 B4 00 \.4D.D4. $B0: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. . . [unchanged] . $F8: AC 00 AC 00 AC 00 AC 00 ,.,.,.,. --------------------------------------- BUFFER 0/SLOT 6/DRIVE 1/MASK OFF/NORMAL --------------------------------------- COMMAND : _ --^-- Moving back to the Copy ][+ nibble editor, the sector looks like this: --v-- COPY ][ PLUS BIT COPY PROGRAM 8.4 (C) 1982-9 CENTRAL POINT SOFTWARE, INC. --------------------------------------- TRACK: 00 START: 1800 LENGTH: 3DFF 2BA8: FF FF FF D5 AA 96 FF FE 2BB0: AA AA AB AF FE FB DE AA 2BB8: EB FF FE FF FF FF FF FF 2BC0: FF FF FF FF FF D5 AA AD 2BC8: 96 96 96 96 96 96 96 96 . . [unchanged] . 2CB8: 96 96 96 96 96 96 E7 E7 ; no 2CC0: E7 EF F3 FC EE E7 FC EE ; timing 2CC8: E7 FC EE EE FC EA E7 E7 ; bits! . . [unchanged] . --------------------------------------- A TO ANALYZE DATA ESC TO QUIT ? FOR HELP SCREEN / CHANGE PARMS Q FOR NEXT TRACK SPACE TO RE-READ --^-- When the protection check looks for a $D5 nibble, it will find it. (It's the first nibble of the data prologue; that hasn't changed.) When the protection check looks for an $E7 nibble, it will find it. (It's at offset $2CB6, after $F6 other nibbles that are all $96. That hasn't changed either.) When it desynchronizes and looks for an $EE nibble, it will find it (after it skips over two desynchronized $FF nibbles) at offset $2CC4. At this point we've resynchronized the bitstream to the original nibble boundary, but the protection code doesn't know that. The next 8 nibbles on disk after $EE (starting at offset $2CC5) are "E7 FC EE E7 FC EE EE FC". That's the magic nibble sequence that the protection check looks for. That's the decryption key. The code thinks it's reading out- of-phase nibbles from an original disk, but it's really reading in-sync nibbles from a specially crafted copy. Not only have we defeated the E7 E7 E7 protection check with no code changes, we've automated the entire process. The only change was that 12-byte patch, and Post-Demuffin Patcher found and applied that patch with no human intervention. It took all of 2 seconds. The rest of this write-up was just to explain what had already happened. It's like I don't even need to be here. ]PR#6 ...works, and it is glorious... Quod erat liberandum. ~ Acknowledgements Thanks to qkumba for doing the heavy lifting here. This entire technique was his idea, and he spent a lot of time getting it to work in modern emulators (with their, um, "special" ways of reading from emulated disk images). I spent a small amount of time minimizing the byte patch. The final result may look simple, but there were a lot of almost-but-not-working solutions along the way. Read all about it here: https://www.alchemistowl.org/pocorgtfo/ pocorgtfo11.pdf --------------------------------------- A 4am crack No. 681 ------------------EOF------------------