Difference between revisions of "Eagles GMA87"

From Software Archive
Jump to navigation Jump to search
(Created page with "= Eagles (C64) — GMA87 copy protection, full technical write-up = This article documents, in full technical detail, the copy protection scheme used by the Commodore 64 rele...")
 
 
Line 2: Line 2:
  
 
This article documents, in full technical detail, the copy protection scheme
 
This article documents, in full technical detail, the copy protection scheme
used by the Commodore 64 release of '''Eagles''' (Hewson/Graeme Ashton,
+
used by the Commodore 64 release of '''Eagles''', as distributed on the ''Beau Jolly Big Box 2'' compilation disk
1987), as distributed on the ''Beau Jolly Big Box 2'' compilation disk
 
 
(<code>BeauJolly_BigBox2_Disk6_s0.g64</code>). Eagles carries a
 
(<code>BeauJolly_BigBox2_Disk6_s0.g64</code>). Eagles carries a
 
<code>GMA87</code> disk label — the same protection family used by
 
<code>GMA87</code> disk label — the same protection family used by

Latest revision as of 00:00, 25 August 2026

Eagles (C64) — GMA87 copy protection, full technical write-up

This article documents, in full technical detail, the copy protection scheme used by the Commodore 64 release of Eagles, as distributed on the Beau Jolly Big Box 2 compilation disk (BeauJolly_BigBox2_Disk6_s0.g64). Eagles carries a GMA87 disk label — the same protection family used by Cholo, Triaxos, and roughly twenty other titles linking to the GMA87 wiki page — but, uniquely among all of them, moves the actual decryption step off the C64 and onto the 1541 disk drive's own 6502 CPU. This makes Eagles a title where the C64 never sees ciphertext for the protected game data at all.

Summary

  • The disk directory is deliberately hidden from naive tools by a non-standard
 ($00 instead of $A0) filename padding.
  • The only loadable file, BOOT, is deliberately loaded at
 $02A7 so that it overlaps and hijacks the C64's own KERNAL
 page-2/3 RAM vector table — a standard auto-start trick.
  • From there, a self-decrypting bootstrap streams in a much larger program
 live from the drive, byte by byte, XORing each byte against its own
 destination address (a descramble, not a secret key).
  • That program contains a hardware-handshake primitive
 ($05EB) that talks to the drive over CIA2's $DD00
 port with cycle-accurate raster-line synchronization. It is used for
 every byte of a further custom byte-transfer protocol.
  • The drive, on command, seeks to track 39 containing a
 deliberately malformed sync region, and measures a **physically
 unclonable, disk-specific signature byte** from sync length variation —
 structurally the same measurement Cholo/Triaxos perform on track 38.
 Measured value for this disk capture: $95 (149 decimal).
  • Unlike Cholo/Triaxos (which send this byte to the C64 and decrypt a large
 static block there in one pass), Eagles uploads a fresh drive-side
 program to drive RAM ($0300$03DF) via
 standard 1541 M-W (memory-write) commands, and bakes the
 measured signature byte directly into that program's own machine code —
 as the literal operand of an EOR #$95 instruction at drive
 address $035D.
  • From that point on, the drive's own 6502 decrypts every byte of
 every subsequent game-data sector, one block at a time, and streams
 already-plaintext bytes to the C64 over the bit-banged serial link. The
 C64 never receives, stores, or applies the key itself.
  • Both halves of the scheme fail silently on a bad copy — a hang, not an
 error message or a crash on load. A disk that reproduces the sync-mark
 layout but not the exact sync length pattern sails through every check on
 both sides and decrypts to garbage far downstream

Disk structure and directory obfuscation

Non-standard filename padding

The BOOT directory entry (logical track 18, sector 1, slot 0) is the four bytes 42 4F 4F 54 ("BOOT") followed by twelve $00 bytes, not the standard CBM DOS $A0 (shifted-space) padding. Two confirmed consequences:

  • LISTing the directory truncates right after BOOT
 with no closing quote or PRG shown — BASIC's line printer
 reads the $00 mid-string as a premature line terminator.
  • LOAD"BOOT",8,1 (exact name) returns FILE NOT FOUND
 — the real KERNAL's name-matching does not tolerate the non-standard
 padding either. Only LOAD"*",8,1 (wildcard match on the
 first directory entry) works.

1306 bytes, load address $02A7.

Boot mechanism: overlapping the KERNAL's own RAM vector table

$02A7 deliberately overlaps $0300$0333, the C64's page-2/3 KERNAL RAM vector table (IERROR, IMAIN, ..., CINV, ..., IGETIN, ...). Simply loading the file overwrites these vectors with whatever bytes land at those file offsets — no explicit install instruction is needed.

  • IMAIN ($0302, the address BASIC jumps through
 every time it returns to its input loop) → becomes $0334.
  • ISTOP ($0328, checked periodically during
 KERNAL I/O, including during LOAD itself) → becomes
 $02ED.

It means the "program" starts running the moment LOAD returns control to BASIC, with no RUN needed. The execution entry is at $02ED (ISTOP), not byte 0 of the file.

Bootstrap: a self-decrypting live stream, not a static file

Tracing forward from $02ED live (the static file content at these addresses is misleading — see why below):

  • $02ED$0301: JSR $FFC3
 (CLOSE), set pointer $AE/$AF =
 $0334, disable the display ($D011), then
 JSR $FFC6 (CHKIN — itself hijacked, see next),
 then JSR $0334.
  • The hijacked ICHKIN vector ($031E
 $02CB) actually points at a small relocator that copies 9
 bytes from $0304 to $8000, then
 JMP $02A7.
  • $02A7: a tight loop —
$02A7  JSR $FFA5        ; ACPTR - read one byte from the IEC bus
$02AA  EOR $AE          ; XOR against the destination pointer's OWN low byte
                         ;   (a self-referential descramble, not a secret
                         ;   key - fully determined by address alone)
$02AC  STA ($AE),Y
$02AE  ...               ; increment pointer, loop
 ...continuing until the pointer reaches $075D. This is why a
 static disassembly of $0334$075D looks like
 noise: those bytes are overwritten live, streamed byte-by-byte from the
 drive, before they are ever executed.

Once populated, $0334 onward contains a real, coherent program

The $05EB primitive: the real protection gate

Deep inside the newly-populated block sits $05EB, the key routine used for every byte of the custom transfer protocol that follows — including the destination address the payload gets written through.

$05EB  A9 0A       LDA #$0A            ; border/background = orange (visual cue)
$05ED  8D 20 D0    STA $D020
$05F0  8D 21 D0    STA $D021
$05F3  A5 17       LDA $17
$05F5  8D 00 DD    STA $DD00           ; write to CIA2 port A (drive-facing)
$05F8  EE 18 D4    INC $D418           ; SID volume flicker, cosmetic
$05FB  AD 00 DD    LDA $DD00
$05FE  10 F8       BPL $05FB           ; wait for bit 7 - a response from the drive
$0600  AD 12 D0    LDA $D012           ; raster line
$0603  C9 31       CMP #$31
$0605  90 06       BCC $060D
$0607  29 06       AND #$06
$0609  C9 02       CMP #$02
$060B  F0 F3       BEQ $0600           ; cycle-accurate raster-synced wait
$060D  A9 02       LDA #$02
$060F  8D 20 D0    STA $D020           ; border = red
$0612  8D 21 D0    STA $D021
$0615  A5 18       LDA $18
$0617  8D 00 DD    STA $DD00
$061A  EA (x9)     NOP                 ; fixed-length timing pad
$0624  AE 00 DD    LDX $DD00           ; read the port value...
$0627  BD 00 01    LDA $0100,X         ; ...as an INDEX into a lookup table
$062A  AE 00 DD    LDX $DD00
$062D  1D 08 01    ORA $0108,X         ; three more reads, three more tables, OR'd together
$0630  AE 00 DD    LDX $DD00
$0633  1D 10 01    ORA $0110,X
$0636  AE 00 DD    LDX $DD00
$0639  1D 18 01    ORA $0118,X
$063C  60          RTS

This reads CIA2's $DD00 port with cycle-accurate raster-line synchronization — structurally the same "cycle-accurate $DD00" handshake mechanism the Cholo/Triaxos track-38 check uses — and decodes the drive's response into a byte using four 256-entry lookup tables as a nibble/bit decoder.

The four lookup tables at $0100/$0108/ $0110/$0118 are fixed, not disk-derived: a store-watchpoint on that range caught the first write coming from a plain, unconditional copy loop ($0725$073F) that tiles a fixed 28-byte source at $0740, eight repeats each, into $0100$01FF. They only gate whether $05EB can complete at all; they don't encode a disk-specific value.

The transfer this primitive serves uses a $01-byte escape/terminator framing: a lone $01 signals end-of-data, $01 $01 is an escaped literal $01, anything else is stored directly via STA ($AE),Y with no further transformation.

Verification: a plain D64 hangs, it does not decrypt to garbage

Attaching a plain sector-image conversion of this disk (no raw GCR track-39 signature present) and running the identical LOAD"*",8,1 sequence, execution reaches $02A7, runs identically up to this exact point, and then hangs permanently at $05FB/$05FE (LDA $DD00 / BPL $05FB).

The signature track: track 39

The signature region is not at logical track 38 the way it is for Cholo/Triaxos. Converting the G64 to text and inspecting it shows physical tracks 36–38 are entirely empty (no track N section at all — a real gap), while track 39 has a section with two unmistakable markers: its first decoded bytes start with 69 59 59 A7 A7... (matching the 69 XX XX XX A9-style signature prefix used by every other GMA87 game), and its sync N entries are wildly non-uniform (41, 40, 40, 40, 87, 136, 40, 40) versus a normal track's uniform sync-mark length throughout.

Track-seek mechanism: entirely standard DOS ROM code

The drive's own DRVTRK variable ($0022) was traced live across a full load. The DRVTRK=39 transition is reached via completely standard, unmodified DOS-ROM job-dispatch code.

  • The custom drive code's only involvement is submitting an ordinary job
 (code $E0, EXECUTE) into the standard job queue with a
 header-table track value of 39 — a value the ROM never validates
 against the normal 1–35 range.
  • The store into DRVTRK happens at $F31B
 (LDA ($32),Y / STA $22), inside the stock job
 dispatcher's buffer-scan/mismatch-handling loop — 100% ROM code, reading
 the out-of-range track value straight out of the job's own header-table
 entry.

Stage 1: the track-39 signature-measurement program

It turns out to be structurally identical to the Cholo/Triaxos track-38 algorithm, just retargeted to Eagles' own signature track (39):

; ---- sync-marker search, 90-attempt retry budget ----
$0300  AD 00 1C    LDA $1C00
$0303  29 9F       AND #$9F
$0305  8D 00 1C    STA $1C00
$0308  A0 5A       LDY #$5A            ; Y = 90 - sync-retry budget
$030A  88          DEY                 ; <-- retry entry point
$030B  D0 05       BNE $0312
$030D  A9 02       LDA #$02            ; error $02 = HEADER NOT FOUND
$030F  4C 69 F9    JMP $F969           ; ERRR - retries exhausted, report failure
$0312  2C 00 1C    BIT $1C00
$0315  30 FB       BMI $0312           ; sync-wait poll
$0317  AD 01 1C    LDA $1C01           ; discard first raw byte after sync
$031A  B8          CLV
$031B  A2 04       LDX #$04
$031D  50 FE       BVC $031D           ; CLV/BVC byte-ready wait (classic 1541 trick)
$031F  B8          CLV
$0320  AD 01 1C    LDA $1C01           ; read raw GCR byte
$0323  9D 00 05    STA $0500,X         ; store into $0500-$0504 (5 bytes)
$0326  CA          DEX
$0327  10 F4       BPL $031D
$0329  C9 A9       CMP #$A9            ; last byte read must be $A9
$032B  D0 DD       BNE $030A           ; mismatch -> retry
$032D  AD 04 05    LDA $0504           ; first byte read (2nd overall)
$0330  C9 69       CMP #$69            ; must be $69
$0332  D0 D6       BNE $030A           ; mismatch -> retry
                                        ; signature "69 xx xx xx A9" confirmed

; ---- 10-sample raw pulse-width (flux-jitter) measurement ----
$0334  A0 00       LDY #$00
$0336  2C 00 1C    BIT $1C00
$0339  30 FB       BMI $0336           ; wait for next sync
$033B  A2 00       LDX #$00
$033D  E8          INX                 ; <-- pulse-width measurement loop
$033E  2C 00 1C    BIT $1C00
$0341  10 FA       BPL $033D           ; count iterations (X) while bit7=0
$0343  8A          TXA
$0344  99 00 05    STA $0500,Y         ; store sample[Y]
$0347  C8          INY
$0348  C0 0A       CPY #$0A            ; 10 samples total
$034A  D0 EA       BNE $0336

; ---- fold 10 samples into an 8-bit result via CMP/ROL ----
$034C  A2 02       LDX #$02            ; sample[0] discarded, sample[1] = reference
$034E  BD 00 05    LDA $0500,X
$0351  CD 01 05    CMP $0501           ; compare sample[X] to reference
$0354  2E 0A 05    ROL $050A           ; roll carry (>=ref->1, <ref->0) into result byte
$0357  E8          INX
$0358  E0 0A       CPX #$0A            ; samples[2..9], 8 comparisons -> 8-bit result
$035A  D0 F2       BNE $034E
$035C  AE 0A 05    LDX $050A           ; X = computed 8-bit result: $95 for this disk

; ---- send the result byte to the C64 via VIA1 $1800, nibble-out ----
$035F  2C 00 18    BIT $1800
$0362  10 FB       BPL $035F           ; wait for IEC bus ready
$0364  A9 10       LDA #$10
$0366  8D 00 18    STA $1800
$0369  2C 00 18    BIT $1800
$036C  30 FB       BMI $0369
$036E  8A          TXA                 ; A = result byte
$036F  4A          LSR A               ; \  send high nibble
$0370  4A          LSR A               ;  |
$0371  4A          LSR A               ;  |
$0372  4A          LSR A               ; /
$0373  8D 00 18    STA $1800
$0376  0A          ASL A
$0377  29 0F       AND #$0F
$0379  8D 00 18    STA $1800
$037C  8A          TXA
$037D  29 0F       AND #$0F            ; \  send low nibble
$037F  8D 00 18    STA $1800           ;  |
$0382  0A          ASL A               ;  |
$0383  29 0F       AND #$0F            ;  |
$0385  8D 00 18    STA $1800           ; /
$0388  A9 0F       LDA #$0F
$038A  EA          NOP
$038B  8D 00 18    STA $1800           ; final bus state
$038E  A9 01       LDA #$01            ; status $01 = SUCCESS
$0390  4C 69 F9    JMP $F969           ; ERRR (used generically as "set status, return")

; ---- self-relocation/patch helper (install-time only, not per-visit) ----
$0393  38          SEC
$0394  AD 39 03    LDA $0339
$0397  E9 E1       SBC #$E1
$0399  85 FB       STA $FB
$039B  AD 3A 03    LDA $033A
$039E  E9 10       SBC #$10
$03A0  85 FC       STA $FC
$03A2  A9 8D       LDA #$8D            ; $8D = STA opcode - patches code in place
$03A4  A0 00       LDY #$00
$03A6  91 FB       STA ($FB),Y
$03A8  C8          INY
$03A9  38          SEC
$03AA  AD 35 03    LDA $0335
$03AD  E9 D2       SBC #$D2
$03AF  91 FB       STA ($FB),Y
$03B1  C8          INY
$03B2  AD 36 03    LDA $0336
$03B5  E9 04       SBC #$04
$03B7  91 FB       STA ($FB),Y
$03B9  60          RTS

; ---- self-decrypt the $0300-$0392 block against 3 fixed ROM bytes (anti-static-disasm) ----
$03BA  A2 00       LDX #$00
$03BC  BD 00 03    LDA $0300,X
$03BF  4D 10 F5    EOR $F510
$03C2  4D 56 F5    EOR $F556
$03C5  4D 18 C1    EOR $C118
$03C8  9D 00 03    STA $0300,X
$03CB  E8          INX
$03CC  E0 93       CPX #$93            ; $93 = 147 bytes
$03CE  D0 EC       BNE $03BC

; ---- job setup: target track 39, submit EXECUTE job, check result ----
$03D0  A9 27       LDA #$27            ; $27 = 39 decimal - THE TARGET TRACK
$03D2  85 06       STA $06
$03D4  A9 01       LDA #$01
$03D6  85 07       STA $07
$03D8  20 18 C1    JSR $C118           ; (ROM call)
$03DB  A9 E0       LDA #$E0
$03DD  85 00       STA $00             ; submit EXECUTE job (buffer 0)
$03DF  A5 00       LDA $00
$03E1  30 FC       BMI $03DF           ; wait for job completion
$03E3  C9 02       CMP #$02
$03E5  90 06       BCC $03ED           ; status < 2 (success) -> RTS normally
$03E7  4C E7 03    JMP $03E7           ; status >= 2 (FAILURE) -> INFINITE SELF-LOOP
$03EA  20 2C C1    JSR $C12C           ; (unreached in the traced path)
$03ED  60          RTS

Both failure modes are deliberate, permanent hangs, never wrong output: if the 5-byte marker doesn't match, the routine retries the sync search up to 90 times before giving up with error $02 ("header not found"). If the job's returned status is failure, execution at $03E7 falls into an infinite self-loop (JMP $03E7) — the drive-side counterpart to the C64-side hangs at $05FB/$05FE.

Measured result byte for this disk capture: $95 (149 decimal). Raw sample buffer at $0500$0509: 12 27 3C 11 12 3D 12 3D 12 3D (sample[0]=$12 discarded, sample[1]=$27 the reference). Hand-verifying the CMP/ROL algorithm against these samples independently reproduces $95 exactly.

Stage 1 → C64: $05EB receives $95

The C64 receives the drive's track-39 result via the same $05EB routine documented above (raster-synced $DD00 poll, then the 4-lookup-table decode, RTS at $063C).

The consumer is a separate, small wrapper elsewhere in the same code block:

$071F  78          SEI
$0720  20 EB 05    JSR $05EB
$0723  58          CLI
$0724  60          RTS ; A=$95, SP=$FB

This returns to

$03B9  8D 6E 05    STA $056E      ; A=$95 (unchanged), SP=$FF

The M-W upload: staging a fresh drive-side program

$056E is not a destination in its own right — it is offset $5D (93 decimal) inside a larger, 224-byte C64-RAM buffer based at $0511. That buffer gets uploaded to the drive, 32 bytes at a time, via a sequence of standard 1541 DOS M-W (memory-write) command-channel commands:

; ---- open the drive's command channel (LFN=15, dev=8, SA=15) ----
$06D4  20 BD FF    JSR $FFBD          ; SETNAM
$06D7  A9 0F       LDA #$0F           ; A = 15
$06D9  A8          TAY                ; Y = 15 (secondary address = command channel)
$06DA  A2 08       LDX #$08           ; X = 8 (device 8)
$06DC  20 BA FF    JSR $FFBA          ; SETLFS(LFN=15, dev=8, SA=15)
$06DF  20 C0 FF    JSR $FFC0          ; OPEN
$06E2  A2 0F       LDX #$0F
$06E4  20 C9 FF    JSR $FFC9          ; CHKOUT(15)
$06E7  A9 4D       LDA #$4D / JSR $FFD2   ; CHROUT 'M'
$06EC  A9 2D       LDA #$2D / JSR $FFD2   ; CHROUT '-'
$06F1  60          RTS                ; "M-" sent so far

; ---- complete the M-W command header and send one 32-byte chunk ----
$0644  20 D2 06    JSR $06D2          ; (the "M-" open routine above)
$0647  A9 57       LDA #$57 / JSR $EDDD   ; 'W'  -> command so far: "M-W"
$064C  A5 17       LDA $17 / JSR $EDDD    ; address LOW byte  (running offset:
                                           ;   $00,$20,$40,$60,$80,$A0,$C0)
$0651  A9 03       LDA #$03 / JSR $EDDD   ; address HIGH byte (fixed: page $03)
$0656  A9 20       LDA #$20 / JSR $EDDD   ; count = $20 (32 bytes), fixed
$065B  A4 17       LDY $17
$065D  18          CLC
$065E  A5 17       LDA $17
$0660  69 20       ADC #$20           ; running offset += $20 for next pass
$0662  85 17       STA $17
$0664  B9 11 05    LDA $0511,Y        ; <-- payload byte Y of the upload buffer
$0667  20 DD ED    JSR $EDDD          ; send it
$066A  C8          INY
$066B  C4 17       CPY $17
$066D  D0 F5       BNE $0664          ; loop for all 32 bytes of this chunk
$066F  20 CC FF    JSR $FFCC          ; CLRCHN
$0672  A5 17       LDA $17
$0674  C9 DA       CMP #$DA           ; $DA = 218 decimal
$0676  90 CC       BCC $0644          ; more chunks needed -> loop
$0678  ...                            ; upload complete (7 chunks, 224 bytes)

Each pass sends one complete M-W $03xx $20 <32 bytes> command. Seven passes, at running offsets $00,$20,$40,$60,$80,$A0,$C0, upload $E0 (224) bytes total, covering drive RAM $0300$03DF. The $0511 buffer is a full replacement copy of the next drive-side job program, staged in C64 RAM and pushed to the drive one M-W chunk at a time.

Since $056E sits at buffer offset $5D, inside the third 32-byte chunk (offsets $40$5F, destined for drive addresses $0340$035F), the measured signature byte lands at drive address $0300 + $5D = $035D.

Stage 2: the uploaded drive-side program, with $95 marked

Uploaded specifically to decrypt and stream the actual game data:

; ---- read a full GCR-encoded sector from the VIA into a 256-byte buffer ----
$0300  A9 06       LDA #$06
$0302  85 31       STA $31
$0304  20 0A F5    JSR $F50A          ; seek/prep (1541 KERNAL DSTRT)
$0307  50 FE       BVC $0307          ; wait for VIA SR byte-ready
$0309  B8          CLV
$030A  AD 01 1C    LDA $1C01          ; read GCR byte from VIA2
$030D  99 00 06    STA $0600,Y
$0310  C8          INY
$0311  D0 F4       BNE $0307          ; fill $0600-$06FF (256 bytes)

$0313  A0 BA       LDY #$BA
$0315  50 FE       BVC $0315
$0317  B8          CLV
$0318  AD 01 1C    LDA $1C01
$031B  99 00 01    STA $0100,Y
$031E  C8          INY
$031F  D0 F4       BNE $0315          ; fill $01BA-$01FF (70 bytes, stack page)

; ---- decode GCR, verify header track and checksum (standard DOS ROM calls) ----
$0321  20 E0 F8    JSR $F8E0          ; GCRBIN
$0324  A5 38       LDA $38
$0326  C5 47       CMP $47            ; header track check
$0328  F0 04       BEQ $032E
$032A  A9 04       LDA #$04
$032C  D0 4E       BNE $037C          ; -> error, status $04

$032E  20 E9 F5    JSR $F5E9          ; CHKBLK
$0331  C5 3A       CMP $3A            ; checksum check
$0333  F0 04       BEQ $0339
$0335  A9 05       LDA #$05
$0337  D0 43       BNE $037C          ; -> error, status $05

; ---- header fields decrypted with FIXED, disk-independent ROM constants ----
$0339  AD 01 06    LDA $0601
$033C  4D 77 F5    EOR $F577          ; byte-count field
$033F  8D 01 06    STA $0601
$0342  85 07       STA $07
$0344  A0 02       LDY #$02
$0346  A2 FF       LDX #$FF
$0348  AD 00 06    LDA $0600
$034B  4D 76 F5    EOR $F576          ; status/type byte
$034E  8D 00 06    STA $0600
$0351  F0 03       BEQ $0356
$0353  8E 01 06    STX $0601
$0356  EE 01 06    INC $0601

; ---- ****** THE PER-BYTE DATA DECRYPTION LOOP ****** ----
$0359  B9 00 06    LDA $0600,Y
$035C  49 95       EOR #$95           ; <====== HERE. The measured
                                       ;   flux-jitter signature byte, INJECTED
                                       ;   by the M-W upload above, sits as the
                                       ;   immediate operand of this EOR at
                                       ;   drive address $035D. Every data byte
                                       ;   of every subsequent sector is
                                       ;   decrypted with THIS key, on the
                                       ;   drive's own 6502, before ever being
                                       ;   sent to the C64.
$035E  AA          TAX
$035F  E0 01       CPX #$01
$0361  D0 03       BNE $0366
$0363  20 91 03    JSR $0391          ; send nibble (bit-bang $1800)
$0366  20 91 03    JSR $0391          ; send nibble
$0369  C8          INY
$036A  CC 01 06    CPY $0601
$036D  D0 EA       BNE $0359          ; loop over the whole decrypted data block

; ---- end-of-block housekeeping / next-sector dispatch ----
$036F  AD 00 06    LDA $0600
$0372  F0 0E       BEQ $0382
$0374  C5 06       CMP $06
$0376  85 06       STA $06
$0378  F0 05       BEQ $037F
$037A  A9 01       LDA #$01
$037C  4C 69 F9    JMP $F969          ; job dispatcher / error exit
$037F  4C 04 03    JMP $0304          ; loop back for next GCR block

$0382  A2 01       LDX #$01
$0384  20 91 03    JSR $0391
$0387  A2 02       LDX #$02
$0389  20 91 03    JSR $0391
$038C  A9 7F       LDA #$7F
$038E  4C 69 F9    JMP $F969          ; job status $7F = done/OK

; ---- $0391: the same $1800 bit-bang nibble-send routine used by Stage 1 ----
$0391  2C 00 18    BIT $1800
$0394  10 FB       BPL $0391
$0396  A9 10       LDA #$10
$0398  8D 00 18    STA $1800
$039B  2C 00 18    BIT $1800
$039E  30 FB       BMI $039B
$03A0  8A          TXA
$03A1  4A          LSR A
$03A2  4A          LSR A
$03A3  4A          LSR A
$03A4  4A          LSR A
$03A5  8D 00 18    STA $1800
$03A8  0A          ASL A
$03A9  29 0F       AND #$0F
$03AB  8D 00 18    STA $1800
$03AE  8A          TXA
$03AF  29 0F       AND #$0F
$03B1  8D 00 18    STA $1800
$03B4  0A          ASL A
$03B5  29 0F       AND #$0F
$03B7  8D 00 18    STA $1800
$03BA  A9 0F       LDA #$0F
$03BD  8D 00 18    STA $1800
$03C0  60          RTS

This is the actual decryption mechanism for the game's data payload. The block's own header fields (byte count, status/type byte) are "decrypted" with fixed, disk-independent constants pulled straight from the drive's own ROM ($F576/$F577) — plain obfuscation, not keyed to this disk at all. Every actual data byte, by contrast, is decrypted with $95 — the disk-specific, sync length variation signature — inside the loop at $0359$036D, entirely on the drive, before the byte is ever placed on the serial bus. The C64 receives only already-decrypted plaintext through the same $0391 nibble-out routine documented for Stage 1.

See also