---------------Hacker II--------------- A 4am crack 2015-08-26 -------------------. updated 2015-09-06 |___________________ Name: Hacker II Genre: games/simulation Year: 1986 Publisher: Activision, Inc. Media: single-sided 5.25-inch floppy OS: The Turbocharger 2.0 (DOS 3.3- compatible) -- T02,S03 has the string "THE TURBOCHARGER VERSION 2.0" Previous cracks: The Dragon Lord ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways COPYA no errors, but copy hangs on boot (after showing DOS prompt) Locksmith Fast Disk Backup ditto EDD 4 bit copy (no sync, no count) ditto Copy ][+ nibble editor nothing suspicious Disk Fixer T00-T02 -> custom bootloader, "The Turbocharger," that loads DOS 3.3 T11 -> regular DOS 3.3 disk catalog T01,S0A -> startup program is "HACKER II HELLO" (binary) 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. The disk loads DOS very quickly, using a third-party DOS modification called "The Turbocharger." But my intuition tells me that's not the problem -- the copy protection lies elsewhere. To confirm this, 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 ... ]BRUN HACKER II HELLO,S6 ...works... [S6,D1=non-working copy] [S5,D1=DOS 3.3 master disk] ]PR#5 ... ]BRUN HACKER II HELLO,S6 ...fails identically to booting the non-working copy directly... Next steps: 1. Trace the startup program 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 [S6,D1=original disk] ]PR#6 ... [ immediately after prompt] ]FP ]CATALOG DISK VOLUME 254 B 007 HACKER II HELLO B 009 DOSPRITES B 030 SPRITE BLOCK B 012 BLOAD/BRUN DOS.D500 B 002 HACKER II OPENING B 028 HACKER II PART 1 B 020 HACKER II PART 1.DAT1 B 003 HACKER II MAIN B 056 HACKER II PART 2 B 024 HACKER II PART 2.DAT1 B 074 HACKER II PART 2.DAT2 B 003 MSG2 B 006 MSG3 B 008 MSG4 B 003 HACKER II COVER B 004 HACKER II PART 3.DAT1 B 034 HACKER II PART 3.DAT2 B 005 HACKER II PART 3.DAT3 B 011 HACKER II PART 3 ]BLOAD HACKER II HELLO ]CALL -151 *AA72.AA73 AA72- 00 60 *6000L ; reset stack, use ROM, clear screen 6000- A2 FF LDX #$FF 6002- 9A TXS 6003- 2C 82 C0 BIT $C082 6006- 20 58 FC JSR $FC58 ; set reset vector 6009- 20 04 61 JSR $6104 ; more standard initialization 600C- 20 84 FE JSR $FE84 600F- 20 2F FB JSR $FB2F 6012- 20 93 FE JSR $FE93 6015- 20 89 FE JSR $FE89 6018- AD 58 C0 LDA $C058 601B- AD 5A C0 LDA $C05A 601E- AD 5D C0 LDA $C05D 6021- AD 5F C0 LDA $C05F 6024- AD FF CF LDA $CFFF 6027- AD 10 C0 LDA $C010 602A- A9 00 LDA #$00 602C- 85 FD STA $FD 602E- 85 FE STA $FE 6030- 85 FF STA $FF ; get address of RWTS parameter table 6032- 20 E3 03 JSR $03E3 6035- 85 05 STA $05 6037- 84 04 STY $04 ; get boot slot (x16) 6039- A0 01 LDY #$01 603B- B1 04 LDA ($04),Y 603D- 8D 9A 61 STA $619A 6040- C8 INY ; and boot drive 6041- B1 04 LDA ($04),Y 6043- 8D 9B 61 STA $619B 6046- 2C 83 C0 BIT $C083 6049- 2C 83 C0 BIT $C083 ; multi-sector read via RWTS 604C- A9 8C LDA #$8C 604E- A0 60 LDY #$60 6050- 20 92 61 JSR $6192 6053- 90 03 BCC $6058 6055- 4C 25 61 JMP $6125 ; execution continues here (from $6053) ; if RWTS read succeeded 6058- 20 78 63 JSR $6378 *6378L ; set reset vector (again) 6378- A9 75 LDA #$75 637A- 8D F2 03 STA $03F2 637D- A9 65 LDA #$65 637F- 8D F3 03 STA $03F3 6382- 49 A5 EOR #$A5 6384- 8D F4 03 STA $03F4 ; an address? ($FA) -> $6578 6387- A9 78 LDA #$78 6389- 85 FA STA $FA 638B- A9 65 LDA #$65 638D- 85 FB STA $FB ; suspicious 638F- A9 C5 LDA #$C5 6391- 48 PHA ; memory move 6392- A9 00 LDA #$00 6394- 85 FC STA $FC 6396- A2 03 LDX #$03 6398- BC B5 63 LDY $63B5,X ; ah, it was an address 639B- 91 FA STA ($FA),Y 639D- CA DEX 639E- 10 F8 BPL $6398 ; suspicious 63A0- 8A TXA 63A1- 48 PHA We've now pushed $C5/$FF to the stack, so an RTS right now would jump to $C600 and reboot slot 6. Let's try to avoid that. 63A2- 20 B9 63 JSR $63B9 *63B9L ; call RWTS to position drive head 63B9- A5 FB LDA $FB 63BB- A4 FA LDY $FA 63BD- 20 56 63 JSR $6356 63C0- A9 00 LDA #$00 63C2- 85 48 STA $48 63C4- 90 02 BCC $63C8 ; on error, pop the real return address ; and leave $C5/$FF at the top of the ; stack, then RTS to reboot 63C6- 68 PLA 63C7- 68 PLA 63C8- 60 RTS My non-working copy didn't reboot, so I guess it got past this. Continuing from $63A5... *63A5L ; get slot number (x16) into X 63A5- A0 01 LDY #$01 63A7- 8C E7 C8 STY $C8E7 63AA- B1 FA LDA ($FA),Y 63AC- AA TAX ; here we go 63AD- 20 C9 63 JSR $63C9 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 *63C9L ; turn on drive motor manually ; (literally never not suspicious) 63C9- BD 89 C0 LDA $C089,X ; initialize Death Counters 63CC- A9 56 LDA #$56 63CE- 85 FD STA $FD 63D0- A9 08 LDA #$08 63D2- C6 FC DEC $FC 63D4- D0 04 BNE $63DA ; if Death Counter hits 0, jump forward ; (more on this in a second) 63D6- C6 FD DEC $FD 63D8- F0 34 BEQ $640E ; look for a specific nibble ($FB) 63DA- BC 8C C0 LDY $C08C,X 63DD- 10 FB BPL $63DA 63DF- C0 FB CPY #$FB 63E1- D0 ED BNE $63D0 63E3- F0 00 BEQ $63E5 ; 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) 63E5- EA NOP 63E6- EA NOP ; read data latch (note: no BPL loop ; here, we're just reading it once) 63E7- 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) 63EA- C0 08 CPY #$08 ; rotate the carry into the low bit of ; the accumulator 63EC- 2A ROL ; if we just rolled a "1" bit out of ; the high bit of the accumulator, take ; this branch 63ED- B0 0B BCS $63FA ; next nibble needs to be $FF 63EF- BC 8C C0 LDY $C08C,X 63F2- 10 FB BPL $63EF ; ...otherwise we start over 63F4- C0 FF CPY #$FF 63F6- D0 D8 BNE $63D0 ; loop back to get next nibble 63F8- F0 EB BEQ $63E5 ; execution continues here (from $63ED) ; get another nibble 63FA- BC 8C C0 LDY $C08C,X 63FD- 10 FB BPL $63FA ; stash it in zero page 63FF- 84 FC STY $FC ; if the accumulator is anything but ; %00001010, start over 6401- C9 0A CMP #$0A 6403- D0 CB BNE $63D0 I got lost several times trying to follow this routine. I think the easiest way to explain it is to show the difference between the original disk and my non-working copy. Here is the original disk, as seen by the Copy II+ nibble editor. Nibbles with extra "0" bits (timing bits) after them are displayed in inverse on an original machine, marked here with a "+" after the nibble. --v-- COPY ][ PLUS BIT COPY PROGRAM 8.4 (C) 1982-9 CENTRAL POINT SOFTWARE, INC. --------------------------------------- TRACK: START: 1B1E LENGTH: 17C1 1C70: 9F EB E5 FC D7 D7 D7 EE VIEW 1C78: FA E6 E6 FF FE F2 ED FD 1C80: FF EF ED BA BB DD AF E6 1C88: B7 A7 CB B7 DE AA EB FF 1C90: FF FF FF FB+FF FF+FF FF+ 1C98: FD FF+FF+FF+FF+FF+FF+FF+ 1CA0: FF+FF+D5 AA 96 AA AB AA 1CA8: AA AA AB AA AA DE AA EB+ 1CB0: FF+FF+FF+FF+FF+FF D5 AA --------------------------------------- A TO ANALYZE DATA ESC TO QUIT ? FOR HELP SCREEN / CHANGE PARMS Q FOR NEXT TRACK SPACE TO RE-READ --^-- It's easy to understand why a simple sector copy failed. The sequence that this code is looking for starts at offset $1C93, which is between the end of one sector and the beginning of the next. (The data epilogue is at $1C8C; the next address prologue is at $1CA2.) Sector copiers discard everything between those delimiters and rebuild the track with a default pattern of sync bytes. That pattern doesn't include an $FB nibble, so the nibble check fails. But the EDD bit copy also failed. Here is the original disk's pattern at offset $1C93: - $FB + timing bit - $FF - $FF + timing bit - $FF - $FF + timing bit And here is what the same part of the track looks like on my failed EDD copy: --v-- COPY ][ PLUS BIT COPY PROGRAM 8.4 (C) 1982-9 CENTRAL POINT SOFTWARE, INC. --------------------------------------- TRACK: START: 1B1E LENGTH: 17C1 1C70: 9F EB E5 FC D7 D7 D7 EE VIEW 1C78: FA E6 E6 FF FE F2 ED FD 1C80: FF EF ED BA BB DD AF E6 1C88: B7 A7 CB B7 DE AA EB FF 1C90: FF FF FF FB+FF FF FF+FF+ 1C98: FD FF+FF+FF+FF+FF+FF+FF+ 1CA0: FF+FF+D5 AA 96 AA AB AA 1CA8: AA AA AB AA AA DE AA EB+ 1CB0: FF+FF+FF+FF+FF+FF D5 AA --------------------------------------- A TO ANALYZE DATA ESC TO QUIT ? FOR HELP SCREEN / CHANGE PARMS Q FOR NEXT TRACK SPACE TO RE-READ --^-- A subtle difference! The sequence at offset $1C93 now looks like this: - $FB + timing bit - $FF - $FF - $FF + timing bit - $FF + timing bit This code is looking for $FF bytes with an alternating pattern of timing bit, no timing bit, timing bit, no timing bit. It doesn't find that on the bit copy, so it knows it's not running on an original disk. Continuing the code listing... ; get a nibble 6405- BD 8C C0 LDA $C08C,X 6408- 10 FB BPL $6405 ; more bit twiddling 640A- 38 SEC 640B- 2A ROL ; AND it with the previously stashed ; nibble 640C- 25 FC AND $FC ; Success path falls through to here, ; but the failure path is also here ; (from $63D8, when the Death Counters ; hit zero). In other words, we will ; always end up here regardless of ; whether the nibble check "passed," ; but the value in the accumulator will ; be wrong if the nibble check failed. 640E- 49 AA EOR #$AA ; The difference between the original ; disk and my non-working copy is right ; here, in the value stored in $6435. ; It's not used right away, but I'll ; bet it will be used soon. 6410- 8D 35 64 STA $6435 ; "This nibble check will self-destruct ; in ten seconds..." 6413- A9 00 LDA #$00 6415- A8 TAY 6416- 99 B9 63 STA $63B9,Y 6419- C8 INY 641A- C0 5D CPY #$5D 641C- D0 F8 BNE $6416 641E- 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. The only difference is the value of $6435. ~ Chapter 3 In Which We See How It All Fits Together Continuing from $63B0 (the next instruction after calling the nibble check)... *63B0L ; pop $C5/$FF off the stack 63B0- 68 PLA 63B1- 68 PLA ; continue elsewhere 63B2- 4C 1F 64 JMP $641F *641FL ; a memory move 641F- A9 87 LDA #$87 6421- 85 FA STA $FA 6423- A9 63 LDA #$63 6425- 85 FB STA $FB 6427- A9 1F LDA #$1F 6429- 85 FC STA $FC 642B- A9 64 LDA #$64 642D- 85 FD STA $FD 642F- 20 5D 65 JSR $655D ; continue elsewhere 6432- 4C 44 64 JMP $6444 *6444L 6444- AD 41 64 LDA $6441 6447- 18 CLC 6448- 6D 35 64 ADC $6435 644B- 85 02 STA $02 644D- AD 42 64 LDA $6442 6450- 38 SEC 6451- ED 35 64 SBC $6435 6454- 85 03 STA $03 6456- AD 43 64 LDA $6443 6459- 4D 35 64 EOR $6435 645C- 85 04 STA $04 There you go: three operations that rely on the correct value in $6435. Luckily, this code is not obfuscated or even difficult to patch. I can do it right now without rebooting. ; switch to ROM and break to monitor *6413:AD 82 C0 4C 59 FF *6000G *6435 6435- 55 The correct value is $55. Now I can hard-code that value on disk. [Disk Fixer] --> "D" for directory mode --> select file "HACKER II HELLO" --> arrows to follow the binary T15,S03,$39 change "00" to "55" I'll skip the nibble check by changing the JSR at $6058 from $6378 (the nibble check) to $641F (the post-check success path): T16,S0C,$5D change "78 63" to "1F 64" Quod erat liberandum. ~ Changelog 2015-09-06 - 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. 424 ------------------EOF------------------