----------------Beer Run--------------- A 4am crack 2015-05-24 -------------------. updated 2015-09-06 |___________________ Name: Beer Run Genre: arcade Year: 1981 Author: Mark Turmell Publisher: Sirius Software Media: single-sided 5.25-inch floppy OS: custom Other versions: The Chief Surgeon / Black Bag crack This game is a single-load... almost. It initially boots to an animated title screen, and game play follows without any disk access. But once the game is over, it reads several tracks from disk before returning to the title screen. The original disk is write-protected (un-notched), so it's not saving high scores. The post-game disk access could be purely copy protection (like Sneakers), or there could be code and data that is only used during the title screen which is reloaded from disk as needed (like Repton and Plasmania). There are two classic file-based cracks of this game. One shows the animated title screen the first time through; the other strips out the title screen altogether. The original disk only boots on an Apple II+ or an unenhanced Apple ][e. Later models appear to load the entire game into memory, then hang. ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways COPYA immediate disk read error Locksmith Fast Disk Backup unable to read any track EDD 4 bit copy (no sync, no count) hangs during boot Copy ][+ nibble editor track 0 has some 4-4 encoded data other tracks are unreadable Disk Fixer nope (can't read 4-4 encoded tracks) Why didn't COPYA work? not a 16-sector disk Why didn't Locksmith FDB work? ditto Why didn't my EDD copy work? I don't know. Could be a nibble check during boot. Could be that the data is loaded from half tracks. Could be both, or neither. Next steps: 1. Trace the boot 2. Capture the game in memory 3. See what's going on with the post- game disk access ~ Chapter 1 In Which We Find A Very Unfriendly "Do Not Disturb" Sign [S6,D1=original disk] [S5,D1=my work disk] ]PR#5 CAPTURING BOOT0 ...reboots slot 6... ...reboots slot 5... SAVING BOOT0 ]BLOAD BOOT0,A$800 ]CALL -151 *801L ; display hi-res graphics page ; (uninitialized) 0801- 8D 50 C0 STA $C050 0804- 8D 52 C0 STA $C052 0807- 8D 54 C0 STA $C054 080A- 8D 57 C0 STA $C057 ; get slot (x16) 080D- A6 2B LDX $2B ; a counter? or an address? 080F- A9 04 LDA #$04 0811- 85 11 STA $11 0813- A0 00 LDY #$00 0815- 84 10 STY $10 ; look for custom prologue ("DD AD DA") 0817- BD 8C C0 LDA $C08C,X 081A- 10 FB BPL $0817 081C- C9 DD CMP #$DD 081E- D0 F7 BNE $0817 0820- BD 8C C0 LDA $C08C,X 0823- 10 FB BPL $0820 0825- C9 AD CMP #$AD 0827- D0 F3 BNE $081C 0829- BD 8C C0 LDA $C08C,X 082C- 10 FB BPL $0829 082E- C9 DA CMP #$DA 0830- D0 EA BNE $081C ; read 4-4 encoded data immediately ; (no address field, no sector numbers) 0832- BD 8C C0 LDA $C08C,X 0835- 10 FB BPL $0832 0837- 38 SEC 0838- 2A ROL 0839- 85 0E STA $0E 083B- BD 8C C0 LDA $C08C,X 083E- 10 FB BPL $083B 0840- 25 0E AND $0E ; ($10) is an address, initialized at ; $080F as $0400 (yes, the text page) 0842- 91 10 STA ($10),Y 0844- C8 INY 0845- D0 EB BNE $0832 0847- E6 11 INC $11 0849- A5 11 LDA $11 ; loop until we hit page 8 (i.e. we're ; filling $0400..$07FF) 084B- C9 08 CMP #$08 084D- D0 E3 BNE $0832 084F- BD 80 C0 LDA $C080,X ; clear $0900..$BFFF in main memory 0852- A9 09 LDA #$09 0854- 85 01 STA $01 0856- A9 00 LDA #$00 0858- 85 00 STA $00 085A- A8 TAY 085B- A2 B7 LDX #$B7 085D- 91 00 STA ($00),Y 085F- C8 INY 0860- D0 FB BNE $085D 0862- E6 01 INC $01 0864- CA DEX 0865- D0 F6 BNE $085D ; calculate a checksum of page 8 (this ; code right here) 0867- 8A TXA 0868- E8 INX 0869- F0 06 BEQ $0871 086B- 5D 00 08 EOR $0800,X 086E- 4C 68 08 JMP $0868 ; use the stack pointer (!) to keep a ; copy of that checksum 0871- AA TAX 0872- 9A TXS ; calculate another checksum of zero ; page 0873- A2 00 LDX #$00 0875- 8A TXA 0876- 55 00 EOR $00,X 0878- E8 INX 0879- D0 FB BNE $0876 ; get slot (x16) again 087B- A6 2B LDX $2B ; jump to the code we just read into ; the text page 087D- 4C 00 04 JMP $0400 Well that's lovely. I need to interrupt the boot at $087D, but if I do, it will modify the checksum that ends up in the stack pointer (which is a great place to stash a checksum as long as you never use PHA, PLA, PHP, PLP, JSR, RTS, or RTI). It's also wiping main memory, including the place I usually put my boot trace callbacks (around $9700). So, a three-pronged attack: 1. Relocate the code to $0900. Most of it uses relative branching already, except for one JMP at $086E, which I can patch. The code will still run, but I'll be able to patch it without altering the checksum. 2. Disable the memory wipe at $095D. 3. Patch the code at $097D to jump to a routine under my control. ~ Chapter 2 In Which Nothing Happens, Inhospitably *9600 *9725 9725- 20 The initial checksum of boot0 is $20. *C500G ... ]CALL -151 *9600 *9733 9733- 01 The second checksum, moved to the stack pointer at $0471, is $01. ~ Chapter 4 Half A Track Is Better Than None Continuing the boot trace at $0472... *C500G ... ]BLOAD BOOT1 0400-07FF,A$2400 ]CALL -151 *2472L 2472- A0 03 LDY #$03 2474- 20 DC 04 JSR $04DC *24DCL ; advance drive head by one phase ; (a.k.a. a half track) 24DC- E6 0C INC $0C 24DE- A5 0C LDA $0C 24E0- 29 03 AND #$03 24E2- 0A ASL 24E3- 05 2B ORA $2B 24E5- AA TAX 24E6- BD 81 C0 LDA $C081,X 24E9- 20 F8 04 JSR $04F8 24EC- BD 80 C0 LDA $C080,X 24EF- 20 F8 04 JSR $04F8 ; loop a number of times (given in the ; Y register on entry) 24F2- 88 DEY 24F3- D0 E7 BNE $24DC 24F5- A6 2B LDX $2B 24F7- 60 RTS 24F8- A9 40 LDA #$40 24FA- 8D 50 C0 STA $C050 24FD- 4C A8 FC JMP $FCA8 We started on track 0 and advanced the drive head by 3 phases, so now we're on track 1.5. ; get target page (given in an array at ; $05D0) 2477- A4 0E LDY $0E 2479- B9 D0 05 LDA $05D0,Y 247C- D0 03 BNE $2481 ; if target page = 0, we're done, so ; continue at $0500 (will get to that ; shortly) 247E- 4C 00 05 JMP $0500 2481- 20 90 04 JSR $0490 *2490L ; set up target page 2490- 85 05 STA $05 2492- 18 CLC ; sector count (4-4 encoded tracks can ; only hold $0C pages worth of data) 2493- A9 0C LDA #$0C 2495- 85 06 STA $06 2497- A0 00 LDY #$00 2499- 84 04 STY $04 ; find custom prologue "DD AD DA" 249B- BD 8C C0 LDA $C08C,X 249E- 10 FB BPL $249B 24A0- C9 DD CMP #$DD 24A2- D0 F7 BNE $249B 24A4- BD 8C C0 LDA $C08C,X 24A7- 10 FB BPL $24A4 24A9- C9 AD CMP #$AD 24AB- D0 F3 BNE $24A0 24AD- BD 8C C0 LDA $C08C,X 24B0- 10 FB BPL $24AD 24B2- C9 DA CMP #$DA 24B4- D0 EA BNE $24A0 ; now read 4-4 encoded data into ($04) 24B6- BD 8C C0 LDA $C08C,X 24B9- 10 FB BPL $24B6 24BB- 38 SEC 24BC- 2A ROL 24BD- 85 0F STA $0F 24BF- 8D 50 C0 STA $C050 24C2- BD 8C C0 LDA $C08C,X 24C5- 10 FB BPL $24C2 24C7- 25 0F AND $0F 24C9- 91 04 STA ($04),Y 24CB- C8 INY 24CC- D0 E8 BNE $24B6 ; increment target page 24CE- E6 05 INC $05 ; decrement sector count 24D0- C6 06 DEC $06 ; Loop back to read more. Note: this ; goes directly to data read routine, ; not the prologue match routine. There ; is only one prologue per track. 24D2- D0 E2 BNE $24B6 24D4- 60 RTS Continuing at $0484... *2484L ; sets Y=2 and falls through to drive ; head advance routine, so this will ; skip ahead 2 phases = 1 whole track, ; so we're still on half tracks but now ; 2.5, 3.5, 4.5, &c. 2484- 20 D8 04 JSR $04D8 ; This routine literally does nothing. ; I'm assuming this boot routine was ; repurposed from other Sirius titles ; that have animated load screens, but ; this disk does not. 2487- 20 00 06 JSR $0600 ; increment page index 248A- E6 0E INC $0E ; and branch back (exits via $0500 when ; the target page = 0) 248C- 4C 77 04 JMP $0477 Here is the target page table (accessed at $0479): *25D0.25DF 25D0- 08 14 60 6C 78 84 90 9C 25D8- A8 B4 00 00 00 00 00 00 To sum up: - We're reading data from consecutive half tracks (1.5, 2.5, 3.5, &c.) - Each track has $0C pages of data in a custom (non-sector-based) format - We're filling main memory ($0800.. $BFFF), except the two hi-res graphics pages ($2000..$5FFF) - Nothing in this read loop relies on the checksum in the stack pointer - $047E exits via $0500 Let's capture it. ~ Chapter 5 In Which The End Is Nigh *9600 *2800<800.1FFFM *C500G ... ]BSAVE BOOT2 0800-1FFF,A$2800,L$1800 ]BRUN TRACE2 ...reboots slot 6... *2000<6000.9FFFM *C500G ... ]BSAVE BOOT2 6000-9FFF,A$2000,L$4000 ]BRUN TRACE2 ...reboots slot 6... *2000, the $FF opcode is "ISC" a.k.a "ISB" a.k.a. "INS". It functions as a combination of INC and SBC. The $FF variant in particular is a 3-byte instruction that accesses an absolute address + X, like so: 2525- FF 00 00 ISC $0000,X which, when executed, does this: INC $0000,X SBC $0000,X Since the accumulator, the X register, and zero page $00 have just been set to zero, this will increment zero page $00 to $01 and subtract that from A. So zero page $00 ends up as $FF, zero page $01 stays at $00, X stays at $00, and A ends up as $FF. Continuing... ; store the result in $00 2528- 85 00 STA $00 ; take the checksum we stashed in the ; stack pointer (at $0471) 252A- BA TSX 252B- 8A TXA ; XOR that with the result of this ; illegal operation 252C- 45 00 EOR $00 ; and put that somewhere in the middle ; of who knows where 252E- 8D 47 0C STA $0C47 ; then XOR that with $FF and put that ; in the middle of God knows what 2531- 49 FF EOR #$FF 2533- 8D 69 0C STA $0C69 ; then start the game 2536- 4C 00 BB JMP $BB00 The checksum in the stack pointer was $01, and zero page $00 is $FF. $01 EOR $FF = $FE --> $0C47 $FE EOR $FF = $01 --> $0C69 ~ Chapter 7 This Isn't Even My Final Form So $BB00 starts the game, right? Wrong. *BLOAD BOOT2 A000-BFFF,A$2000 *FE89G FE93G *A000<2000.3FFFM *BB00L BB00- A6 2B LDX $2B ; copies $6200 page to $BE00 BB02- 20 92 BC JSR $BC92 BB05- A9 60 LDA #$60 ; turns on drive motor and waits BB07- 20 E0 BC JSR $BCE0 ; advance drive head to phase $17 ; (that's a half track -- even phases ; are whole tracks, odd phases are half ; tracks) BB0A- A9 17 LDA #$17 BB0C- 20 48 BB JSR $BB48 ; Reads a track of data ($0C pages) ; using the same RWTS as the routine ; at $0490. Stores it starting at ; $4000 (based on the accumulator on ; entry). BB0F- A9 40 LDA #$40 BB11- 20 F0 BB JSR $BBF0 ; advance drive head to phase $19 BB14- A9 19 LDA #$19 BB16- 20 48 BB JSR $BB48 ; read another track, into $4C00..$57FF BB19- A9 4C LDA #$4C BB1B- 20 F0 BB JSR $BBF0 ; advance drive head to phase $1B BB1E- A9 1B LDA #$1B BB20- 20 48 BB JSR $BB48 ; read another track, into $5800..$63FF BB23- A9 58 LDA #$58 BB25- 20 F0 BB JSR $BBF0 ; turn off drive motor BB28- BD 88 C0 LDA $C088,X ; checksum everything we just read BB2B- A0 00 LDY #$00 BB2D- 84 00 STY $00 BB2F- A9 40 LDA #$40 BB31- 85 01 STA $01 BB33- A2 24 LDX #$24 BB35- 98 TYA BB36- 51 00 EOR ($00),Y BB38- C8 INY BB39- D0 FB BNE $BB36 BB3B- E6 01 INC $01 BB3D- CA DEX BB3E- D0 F6 BNE $BB36 BB40- A8 TAY ; branch back to try again if the ; checksum fails BB41- D0 C2 BNE $BB05 BB43- 4C A1 BC JMP $BCA1 *BCA1L ; copy a page back to $6200 (was copied ; there at $BB02) BCA1- A0 00 LDY #$00 BCA3- B9 00 BE LDA $BE00,Y BCA6- 99 00 62 STA $6200,Y BCA9- 88 DEY BCAA- D0 F7 BNE $BCA3 ; start the game at the title screen BCAC- 4C 00 40 JMP $4000 If I replace the illegal instruction at $0525 with an equivalent sequence of instructions, maybe I can get this game to boot on my enhanced //e. Then I can interrupt it at $BCAC and capture the final form of the game code, including the final chunk at $4000. One small speed bump... the game loader overwrites my boot tracer at $9600, so I'll need to copy the last stage of my trace into the text page instead of jumping back to my code. *9600 (I'm going to re-save all the chunks, since the disk routines that were called from $BB00 modified some of the values in $C00 page and copied some other pages back and forth and I kind of lost track of it all. Anyway, I have the "final" version of the game in memory, so let's just save it all.) *2000<800.1FFFM *C500G ... ]BSAVE OBJ.0800-1FFF,A$2000,L$1800 ]BRUN TRACE4 ...reboots slot 6... *C500G ... ]BSAVE OBJ.4000-5FFF,A$4000,L$2000 ]BRUN TRACE4 ...reboots slot 6... *2000<6000.9FFFM *C500G ... ]BSAVE OBJ.6000-9FFF,A$2000,L$4000 ]BRUN TRACE4 ...reboots slot 6... *2000