------------The Quarter Mile----------- A 4am crack 2015-01-08 --------------------------------------- The Quarter Mile is a 1987 educational game by Daniel Barnum and distributed by Barnum Software. It is preserved here for the first time. The main program is self-contained on a single floppy, but it also supports "accessory disks" that offer additional games on different topics. Once you boot the main program disk, you can swap in an accessory disk at the main menu. I have three such accessory disks: * "Fractions I", dated 1987-09-23 * "Integers I" dated 1988-04-11 * "Equations I", dated 1988-09-28 The in-game news bulletin makes reference to other accessory disks that cover decimals, whole numbers, and percents. If anyone comes across those, I hope this write-up is useful in preserving them. _________________________ { } { "It's so overt, it's } { covert." } { } { Sherlock Holmes: } { Game of Shadows } {_________________________} COPYA fails on the program disk about halfway through. Locksmith Fast Disk Backup shows why COPYA failed: it can read everything except track $11. EDD 4 bit copy gives no read errors, but the copy does not work. It boots into DOS, displays a prompt, then fills the screen with null bytes and crashes. Each accessory disk gives the same results as the program disk: COPYA fails halfway through, and Locksmith Fast Disk Backup skips track $11. Turning to my trusty Disk Fixer sector editor, I can't find any evidence of a standard DOS 3.3 disk catalog anywhere. Maybe it's on the unreadable track $11? The original disk sounds like a DOS 3.3 disk during boot, and the first few tracks look like DOS 3.3 (to a first approximation). Time for boot tracing with AUTOTRACE. [S6,D1=original program disk] [S5,D1=my work disk] ]PR#5 ... CAPTURING BOOT0 ...reboots slot 6... ...reboots slot 5... SAVING BOOT0 CAPTURING BOOT1 ...reboots slot 6... ...reboots slot 5... SAVING BOOT1 SAVING RWTS AUTOTRACE captures everything up to and including the RWTS. But poking through the RWTS, I don't see any evidence of anything that could read track $11. The boot1 code is slightly unusual, but I don't see any nibble checks that would explain why my copy behaves differently than the original disk. Boot0 jumps to boot1 via ($08FD), and boot1 jumps to the usual $9D84 entry point to warm- start DOS. Time to trace further. ]CALL -151 *9600 *0.1 0000- D9 44 The program's entry point is at $44D9. For posterity, I'm going to reboot to my work disk and save the contents of memory. The author went to great length to prevent anyone from seeing it. Who knows, it might come in handy. *C500G ... ]BSAVE QM.OBJ 2000-5FFF,A$2000,L$4000 ]PR#6 ... *2000<6000.9FFFM *C500G ... ]BSAVE QM.OBJ 6000-9FFF,A$2000,L$4000 ]PR#6 ... And before I forget, let's restore the original code to jump to ($0000)... T01,S01,$DE change "4C 59 FF" back to "6C 00 00" Now I can start tracing the main program to figure out why my copy still doesn't work. ]PR#5 ... ]BLOAD QM.OBJ 2000-5FFF,A$2000 ]BLOAD QM.OBJ 6000-9FFF,A$6000 ]CALL -151 *44D9L 44D9- AD 00 40 LDA $4000 44DC- C9 18 CMP #$18 44DE- D0 24 BNE $4504 *4000 4000- 18 OK, so this branch is not taken. (On further reflection, I'm betting this is a kind of "first-run vs. second-run" thing where it needs to do some initialization the first time.) ; set Ctrl-Y vector (WTF) 44E0- 20 3F 40 JSR $403F ; save part of zero page 44E3- 20 32 43 JSR $4332 ; this is just a memory move 44E6- A9 49 LDA #$49 44E8- 85 00 STA $00 44EA- A9 88 LDA #$88 44EC- 85 01 STA $01 44EE- A9 00 LDA #$00 44F0- 85 02 STA $02 44F2- A9 09 LDA #$09 44F4- 85 03 STA $03 44F6- A9 00 LDA #$00 44F8- 85 04 STA $04 44FA- A9 04 LDA #$04 44FC- 85 05 STA $05 44FE- 20 74 43 JSR $4374 ; continue below (part of the first-run ; vs. second-run logic) 4501- 4C 0D 45 JMP $450D ; [skipped] ; 4504- 20 55 40 JSR $4055 ; 4507- 20 90 40 JSR $4090 ; 450A- 4C 40 45 JMP $4540 450D- EA NOP 450E- A9 00 LDA #$00 4510- 8D A1 03 STA $03A1 ; disk read via RWTS 4513- 20 C9 5C JSR $5CC9 4516- AD F6 1F LDA $1FF6 4519- 8D A7 03 STA $03A7 451C- AD F7 1F LDA $1FF7 451F- 8D A2 03 STA $03A2 4522- AD F8 1F LDA $1FF8 4525- 8D A1 03 STA $03A1 4528- AD F9 1F LDA $1FF9 452B- 8D CA 03 STA $03CA ; hmm 452E- AD FF 1F LDA $1FFF 4531- C9 01 CMP #$01 4533- F0 03 BEQ $4538 4535- 20 8C 41 JSR $418C I can't tell (without formally tracing) what the value of $1FFF is at this point, but I can guess whether I want to take that branch by looking at the routine at $418C: *418CL 418C- EA NOP 418D- A9 00 LDA #$00 418F- 85 00 STA $00 4191- A9 04 LDA #$04 4193- 85 01 STA $01 4195- A0 00 LDY #$00 4197- A9 00 LDA #$00 4199- 91 00 STA ($00),Y 419B- E6 00 INC $00 419D- D0 02 BNE $41A1 419F- E6 01 INC $01 41A1- A5 00 LDA $00 41A3- C9 8C CMP #$8C 41A5- D0 F0 BNE $4197 41A7- A5 01 LDA $01 41A9- C9 41 CMP #$41 41AB- D0 EA BNE $4197 ; &c. &c. &c. That's another version of The Badlands; it wipes most of main memory (except itself) and crashes. But I'm positive I haven't seen any routines up to this point that would qualify as copy protection. I think this is just a self-integrity check to ensure that the program code is intact (and hasn't been tampered with). Continuing... 4538- 20 F2 47 JSR $47F2 453B- F0 03 BEQ $4540 453D- 20 A1 4C JSR $4CA1 Same pattern as before... I wonder what we're skipping over this time. *4CA1L ; Holy paranoia, Batman! It's a *third* ; version of The Badlands, wiping most ; of main memory and crashing. 4CA1- EA NOP 4CA2- A9 00 LDA #$00 4CA4- 85 00 STA $00 4CA6- A9 04 LDA #$04 4CA8- 85 01 STA $01 4CAA- A0 00 LDY #$00 4CAC- A9 00 LDA #$00 4CAE- 91 00 STA ($00),Y 4CB0- E6 00 INC $00 4CB2- D0 02 BNE $4CB6 4CB4- E6 01 INC $01 4CB6- A5 00 LDA $00 4CB8- C9 A1 CMP #$A1 4CBA- D0 F0 BNE $4CAC 4CBC- A5 01 LDA $01 4CBE- C9 4C CMP #$4C 4CC0- D0 EA BNE $4CAC 4CC2- A9 C2 LDA #$C2 4CC4- 85 00 STA $00 4CC6- A9 4C LDA #$4C 4CC8- 85 01 STA $01 4CCA- 4C AC 4C JMP $4CAC ; &c. &c. &c. OK, so what's at $47F2? Another self- integrity check? Two in a row? At this point, I'd believe anything. This guy *really* doesn't want anyone tampering with his code. *47F2L 47F2- A9 11 LDA #$11 47F4- 8D 33 02 STA $0233 47F7- 20 06 54 JSR $5406 47FA- AD 00 10 LDA $1000 47FD- C9 96 CMP #$96 47FF- 60 RTS I don't know what's going on here, but most of it is happening in $5406. *5406L 5406- EA NOP 5407- 20 52 54 JSR $5452 *5452L ; Seek (via RWTS call) to the track ; specified in $0233, which was set to ; $11 in the caller. Hey, that's the ; unreadable track! 5452- 20 E3 03 JSR $03E3 5455- 84 00 STY $00 5457- 85 01 STA $01 5459- AD 33 02 LDA $0233 ; $11 545C- A0 04 LDY #$04 545E- 91 00 STA ($00),Y 5460- A9 00 LDA #$00 ; seek 5462- A0 0C LDY #$0C ; cmd 5464- 91 00 STA ($00),Y 5466- A9 00 LDA #$00 5468- A0 03 LDY #$03 546A- 91 00 STA ($00),Y 546C- 20 E3 03 JSR $03E3 546F- 20 D9 03 JSR $03D9 ; go 5472- A9 00 LDA #$00 5474- 85 48 STA $48 5476- 60 RTS Popping the stack to $540A... ; get slot number (x16) from RWTS table ; (previous subroutine left pointer to ; the RWTS parameter table in $00/$01) 540A- A0 01 LDY #$01 540C- B1 00 LDA ($00),Y 540E- AA TAX ; turn on drive motor manually (never ; not suspicious) 540F- BD 89 C0 LDA $C089,X 5412- BD 8E C0 LDA $C08E,X ; sets ($00) to point to $1000 5415- 20 BE 52 JSR $52BE ; skip over non-sync bytes 5418- A0 00 LDY #$00 541A- EA NOP 541B- BD 8C C0 LDA $C08C,X 541E- 10 FB BPL $541B 5420- C9 FF CMP #$FF 5422- D0 F7 BNE $541B ; look for a sequence of sync bytes ; (at least $0F in a row) 5424- A0 0F LDY #$0F 5426- BD 8C C0 LDA $C08C,X 5429- 10 FB BPL $5426 542B- C9 FF CMP #$FF 542D- D0 EB BNE $541A 542F- 88 DEY 5430- D0 F4 BNE $5426 ; skip over any extra sync bytes 5432- BD 8C C0 LDA $C08C,X 5435- 10 FB BPL $5432 5437- C9 FF CMP #$FF 5439- F0 F7 BEQ $5432 ; now store the raw nibbles at ($00), ; which points to $1000 543B- BD 8C C0 LDA $C08C,X 543E- 10 FB BPL $543B 5440- 91 00 STA ($00),Y 5442- E6 00 INC $00 5444- D0 F5 BNE $543B 5446- E6 01 INC $01 5448- A5 01 LDA $01 ; looks like we're storing 16 pages of ; raw nibbles ($1000..$1FFF) 544A- C9 20 CMP #$20 544C- 90 ED BCC $543B ; turn off drive motor and exit 544E- BD 88 C0 LDA $C088,X 5451- 60 RTS Let's revisit the caller in its entirety, filling in the pieces with what we've learned so far. *47F2L ; set track number to seek 47F2- A9 11 LDA #$11 47F4- 8D 33 02 STA $0233 ; read raw nibbles from track $11 into ; $1000..$1FFF 47F7- 20 06 54 JSR $5406 ; look at the first one 47FA- AD 00 10 LDA $1000 47FD- C9 96 CMP #$96 47FF- 60 RTS Popping the stack again, to $4538... *4538L ; read unreadable track $11, compare ; the first non-sync byte to $96 4538- 20 F2 47 JSR $47F2 ; if equal, continue 453B- F0 03 BEQ $4540 ; if not equal, fail catastrophically 453D- 20 A1 4C JSR $4CA1 And there it is. It reads $1000 nibbles from track $11 and compares the first one. ; skip the call to $5406 entirely *47F7:2C ; patch the comparison check so it's ; always equal and the caller's BEQ ; branch is always taken *47FA:EA A9 96 But how can I save this patch? All of this code is encrypted on disk and decrypted in memory by the loader. Oh hey, I have the decrypted version, because I saved it off after tracing the loader (and letting the decryption loop run). So let's write that back to disk and patch the loader so it doesn't decrypt the already-decrypted code. The loader uses standard RWTS calls to read code from the disk. It reads in decreasing sector order but in increasing page order. That is, it starts at T03,S0F and address $2000 and works its way down (sectors) and up (addresses). So track $05, sector $0F is $4000..$40FF, sector $0E is $4100.. $41FF, &c. 08C0- A9 08 LDA #$08 08C2- A0 E8 LDY #$E8 08C4- 20 D9 03 JSR $03D9 08C7- AC ED 08 LDY $08ED 08CA- 88 DEY 08CB- 10 05 BPL $08D2 08CD- A0 0F LDY #$0F 08CF- EE EC 08 INC $08EC 08D2- 8C ED 08 STY $08ED 08D5- EE F1 08 INC $08F1 08D8- CE E1 08 DEC $08E1 08DB- D0 E3 BNE $08C0 08DD- 60 RTS +-- sector count v 08E0- 00 10 00 00 00 00 00 00 08E8- 01 60 01 00 05 0F FB 08 ^ ^ track --+ +-- sector 08F0- 00 40 00 00 02 00 FE 60 ^ +-- starting address 08F8- 01 00 00 00 01 EF D8 00 *BSAVE WRITE DECRYPTED CODE,A$8C0,L$40 [S6,D1=non-working copy] *8C0G ...write write write... Turning to my trusty Disk Fixer sector editor, I need to go back to the custom loader at $A291 and prevent it from decrypting this code, since it's now already decrypted on disk. That routine was at $A542, which is on T01,S04. T01,S04,$42 change "EA" to "60" ]PR#6 The game loads and runs without any further complaint. All options (including saving games and creating data disks) are fully functional. Except one... There is a main menu option called "BULLETIN". On the original disk, it displays several messages, one screen at a time, like so: --v-- %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % % MAIN BULLETIN 3/31/88 % % % % WELCOME TO THE QUARTER MILE 2.3 % % % % VERSION 2.0 (OR GREATER) OF THE % % QUARTER MILE WILL ACCEPT % % ACCESSORY DISKS, WHICH ENABLE % % YOU TO PLAY THE GAME USING A % % WIDER VARIETY OF EDUCATIONAL % % TOPICS (PROBLEM SETS). % % % % ACCESSORY DISKS ARE NOT % % EXPENSIVE AND CAN BE PURCHASED % % AS NEEDED. % % % % % % % % % % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% =CONTINUE =ESCAPE --^-- If you swap in an accessory disk and select "BULLETIN", it print a different set of messages. For example, here is the first bulletin screen from the "Fractions I" accessory disk: --v-- %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % % ACCESSORY DISK 9/23/87 % % FRACTIONS I % % AGE 10 AND UP % % % % THIS DISK INCLUDES 9 % % INTRODUCTORY TOPICS AND 4 TOPICS % % ON ADDING AND SUBTRACTING WITH % % LIKE DENOMINATORS. % % % % THE "INTRO" TOPICS ARE GOOD % % PREPARATION FOR ADDING, % % SUBTRACTING, MULTIPLYING AND % % DIVIDING WITH FRACTIONS. % % % % "FRACTIONS II" CONTINUES WHERE % % THIS DISK LEAVES OFF. % % % % % % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% =CONTINUE =ESCAPE --^-- But on my copy, I get no such messages (neither from the main program disk nor the accessory disks). When I select "BULLETIN", it reads the disk as if it's loading the bulletin text, but it just spews garbage within the border box of "%" characters. This is not a critical bug. The game itself is still playable, even with the accessory disks. But it is definitely a bug, and a quite visible one at that. Somehow I have either triggered it or caused it or missed something somehow somewhere. This is clearly unacceptable. After minutes of intensive research, I can summarize the situation as follows: * If I boot the original disk, leave it in, and select "BULLETIN", it reads the disk and displays the messages correctly. * If I boot my copy, leave it in, and select "BULLETIN", it reads the disk and displays garbage. * If I boot the original disk to the main menu, swap in my copy, and select "BULLETIN", it reads my copy and displays garbage. * If I boot my copy to the main menu, swap in the original disk, and select "BULLETIN", it reads the (original) disk and displays the messages correctly. I should turn that into a 2 x 2 chart, but I'm tired of making ASCII art. But that last finding is very significant. It tells me that I don't have a code problem; I have a data problem. The code on my copy is fine. All of the decryption and writing back decrypted code to the disk and disabling multiple routines during boot and skipping the nibble reading and hard-coding that comparison check... none of that prevents my copy from reading the bulletin data from the original disk and displaying it correctly. My copy just doesn't have the data. Because it's on track $11. It must be. That's the only remaining difference between my copy and the original disk. Locksmith Fast Disk Backup couldn't read track $11, so it just wrote 16 sectors worth of null bytes and moved on. Now any disk (even the original) that tries to display a bulletin from my copy reads something from the disk and spews garbage. I already know there's a routine to read track $11 -- it's at $5406. But I disabled that call, thinking it was purely copy protection. Could that why I'm seeing garbage in the bulletin? No, that doesn't make sense. When I boot my copy and swap in the original disk at the main menu, I see the correct text in the bulletin. That means it must be loading the bulletin text when I select "BULLETIN" from the main menu (at which point it's reading it from the original disk). Since I wrote the decrypted code back to my copy, I can easily scan my copy for the hex sequence "20 06 54". Lo and behold, Disk Fixer finds a match on T06,S0D, at byte offset $AC. That's loaded into memory at $52AC. ]PR#5 ... ]BLOAD QM.OBJ 2000-5FFF,A$2000 ]BLOAD QM.OBJ 6000-9FFF,A$6000 ]CALL -151 And there it is, right where I thought: another call to the nibble-reading routine at $5406. It's part of a larger subroutine that starts at $52A1 (based on the fact that $52A0 is an RTS). *52A0L 52A0- 60 RTS ; again, setting up to seek to the ; unreadable track $11 52A1- A9 11 LDA #$11 52A3- 8D 33 02 STA $0233 ; these two subroutines just print a ; border around the text screen and ; some instructions at the bottom 52A6- 20 C7 52 JSR $52C7 52A9- 20 E6 52 JSR $52E6 ; this reads raw nibbles from track $11 ; into $1000..$1FFF 52AC- 20 06 54 JSR $5406 ; this is where things get interesting 52AF- 20 77 54 JSR $5477 *5477L 5477- EA NOP ; reset ($00) pointer to $1000 5478- 20 BE 52 JSR $52BE 547B- A0 00 LDY #$00 ; get a raw nibble 547D- B1 00 LDA ($00),Y ; decrypt raw nibble into readable text ; (really) 547F- 20 90 54 JSR $5490 ; store it back 5482- 91 00 STA ($00),Y ; and loop through all the nibbles ; (16 pages worth, $1000..$1FFF) 5484- C8 INY 5485- D0 F6 BNE $547D 5487- E6 01 INC $01 5489- A5 01 LDA $01 548B- C9 20 CMP #$20 548D- D0 EE BNE $547D 548F- 60 RTS ; This is the nibble-to-character ; conversion routine. It finds the ; index of the nibble in the standard ; nibble table at $BA29... 5490- A2 3F LDX #$3F 5492- DD 29 BA CMP $BA29,X 5495- F0 08 BEQ $549F 5497- CA DEX 5498- 10 F8 BPL $5492 549A- A9 BF LDA #$BF 549C- 4C A3 54 JMP $54A3 ; ...then converts the index to a ; printable character by offsetting it ; by $A0 (the space character). 549F- 8A TXA 54A0- 18 CLC 54A1- 69 A0 ADC #$A0 54A3- 60 RTS It's so overt, it's covert. The unreadable track $11 isn't (just) for copy protection after all. It has actual data on it: a completely non- sector-based stream of nibbles that are converted to printable text and then printed. Track $11 *is* the bulletin. Now it all makes sense. Obviously my Locksmith Fast Disk Backup copies didn't preserve this data. Locksmith couldn't read the track at all, so it just skipped it. I originally thought nothing of it. Lots of disks dedicate an entire track to their copy protection scheme; once I bypass the protection, the empty track is just ignored. But this track isn't empty at all. Popping the stack and continuing the listing at $52B2... ; reset the ($00) pointer again 52B2- 20 BE 52 JSR $52BE ; this subroutine prints one message at ; a time and waits for a keypress 52B5- 20 46 53 JSR $5346 ; now branch back if there is another ; message to print and the user hasn't ; pressed ESC to cancel 52B8- D0 FB BNE $52B5 ; reset text borders and clear screen 52BA- 20 F3 53 JSR $53F3 52BD- 60 RTS There's nothing in this routine that clears the messages from memory after it's done. In fact, this entire routine is self-contained. That gives me a crazy idea. [S6,D1=my work disk] *C600G ... ]BLOAD QM.OBJ 2000-5FFF,A$2000 ]BLOAD QM.OBJ 6000-9FFF,A$6000 ]CALL -151 [S6,D1=original program disk] *54A1G ...loads bulletin and displays it... ...ESC takes me back to the monitor... *FC58G N 400<1000.13FFM ...screen fills with bulletin text... [S6,D1=my work disk] *BSAVE BULLETIN DECRYPTED,A$1000,L$1000 And just like that, I have the bulletin text from the unreadable track $11, converted to printable text and saved as a file. (I repeated the procedure with each accessory disk. Remember those? They each have their own bulletin, which is stored in the same way on track $11. Which is brilliant, by the way. A blog distributed on floppy disks.) I find myself faced with a wonderful confluence of coincidences: * Each bulletin is exactly 16 pages worth of data ($1000..$1FFF) * There are exactly 16 sectors free on each disk (track $11) * There is already a subroutine (at $5406) dedicated to reading the bulletin text The obvious solution is to write out the bulletin text to the main program disk and each accessory disk as standard sectors on track $11. Then I can modify the subroutine at $5406 to read those sectors into $1000..$1FFF via standard RWTS calls. 08C0- A9 08 LDA #$08 08C2- A0 E8 LDY #$E8 08C4- 20 D9 03 JSR $03D9 08C7- AC ED 08 LDY $08ED 08CA- 88 DEY 08CB- 10 05 BPL $08D2 08CD- A0 0F LDY #$0F 08CF- CE EC 08 DEC $08EC 08D2- 8C ED 08 STY $08ED 08D5- CE F1 08 DEC $08F1 08D8- CE E1 08 DEC $08E1 08DB- D0 E3 BNE $08C0 08DD- 60 RTS +-- sector count v 08E0- 00 10 00 00 00 00 00 00 08E8- 01 60 01 00 11 0F FB 08 ^ ^ track --+ +-- sector 08F0- 00 1F 00 00 02 00 FE 60 ^ +-- starting address *BSAVE WRITE BULLETIN,A$8C0,L$40 *BLOAD BULLETIN DECRYPTED,A$1000 [S6,D1=program disk copy] *8C0G I repeated this procedure for each accessory disk. The files are stored on my work disk as * FRACTIONS BULLETIN DECRYPTED * INTEGERS BULLETIN DECRYPTED * EQUATIONS BULLETIN DECRYPTED Now that the decrypted text is stored in standard sectors, I need to rewrite the routine at $5406 (on T06,S0B) so it uses standard RWTS calls to read track $11 into $1000..$1FFF. I wrote this directly in a sector editor, so here is its code listing with my comments inline: ---------- DISASSEMBLY MODE ----------- 0006:EA NOP ; seek to track $11 (unchanged) 0007:20 52 54 JSR $5452 ; starting sector ($0F) 000A:A0 05 LDY #$05 000C:A9 0F LDA #$0F 000E:91 00 STA ($00),Y ; starting address ($1F00) 0010:A0 08 LDY #$08 0012:A9 00 LDA #$00 0014:91 00 STA ($00),Y 0016:C8 INY 0017:A9 1F LDA #$1F 0019:91 00 STA ($00),Y ; read command ($01) 001B:A0 0C LDY #$0C 001D:A9 01 LDA #$01 001F:91 00 STA ($00),Y ; any disk volume 0021:A0 03 LDY #$03 0023:A9 00 LDA #$00 0025:91 00 STA ($00),Y ; read sector via RWTS 0027:20 E3 03 JSR $03E3 002A:20 D9 03 JSR $03D9 ; decrement address 002D:A0 09 LDY #$09 002F:B1 00 LDA ($00),Y 0031:38 SEC 0032:E9 01 SBC #$01 0034:91 00 STA ($00),Y ; decrement sector 0036:A0 05 LDY #$05 0038:B1 00 LDA ($00),Y 003A:38 SEC 003B:E9 01 SBC #$01 003D:91 00 STA ($00),Y ; loop until we've read all 16 sectors 003F:10 E6 BPL $0027 0041:A9 00 LDA #$00 0043:85 48 STA $48 0045:60 RTS ; now unused space 0046:00 BRK 0047:00 BRK 0048:00 BRK 0049:00 BRK 004A:00 BRK 004B:00 BRK 004C:00 BRK 004D:00 BRK 004E:00 BRK 004F:00 BRK 0050:00 BRK 0051:00 BRK Last but not least, I need to disable the nibble-to-text conversion routine at $5477, which is no longer needed (since the bulletin text is already stored as printable characters). T06,S0B,$77 change "EA" to "60" Quod erat liberandum. --------------------------------------- A 4am crack No. 182 ------------------EOF------------------