💾 Archived View for mirrors.apple2.org.za › active › 4am › images › games › rpg › hacker › Hacker%20… captured on 2024-08-19 at 03:44:30.

View Raw

More Information

-=-=-=-=-=-=-

---------------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
...

[<Ctrl-Reset> 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- 00 60



; 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



; 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



; 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...



; 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




; 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)...



; pop $C5/$FF off the stack
63B0-   68          PLA
63B1-   68          PLA

; continue elsewhere
63B2-   4C 1F 64    JMP   $641F



; 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



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

<beep>



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------------------